Feat/sidecar components (#1578)

* Added a new eslint plugin in TypeScript for Effect components

* Fixed edge cases

* Fixed lint

* Fix eslint

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau 2023-09-15 02:04:45 +02:00 committed by GitHub
parent 09db29c91a
commit 84a27b148f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 4201 additions and 49 deletions

View File

@ -5,7 +5,7 @@ module.exports = {
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin', 'unused-imports', 'simple-import-sort', 'twenty'],
plugins: ['@typescript-eslint/eslint-plugin', 'unused-imports', 'simple-import-sort', 'twenty', 'twenty-ts'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
@ -50,6 +50,7 @@ module.exports = {
'twenty/no-hardcoded-colors': 'error',
'twenty/styled-components-prefixed-with-styled': 'error',
'twenty/matching-state-variable': 'error',
'twenty-ts/effect-components': 'error',
'func-style':['error', 'declaration', { 'allowArrowFunctions': true }],
"@typescript-eslint/no-unused-vars": "off",
"no-unused-vars": "off",

View File

@ -69,13 +69,15 @@
"test": "craco test",
"coverage": "craco test --coverage .",
"lint": "eslint src --max-warnings=0",
"lint:setup": "cd ../packages/eslint-plugin-twenty-ts/ && yarn && yarn build && cd ../../front/ && yarn upgrade eslint-plugin-twenty-ts",
"storybook:dev": "storybook dev -p 6006 -s ../public",
"storybook:test": "test-storybook",
"storybook:test-slow": "test-storybook --maxWorkers=3",
"storybook:build": "storybook build -s public",
"storybook:coverage": "test-storybook --coverage --maxWorkers=3 && npx nyc report --reporter=lcov -t coverage/storybook --report-dir coverage/storybook --check-coverage",
"graphql:generate": "dotenv cross-var graphql-codegen --config codegen.js",
"chromatic": "dotenv cross-var npx chromatic --project-token=$CHROMATIC_PROJECT_TOKEN"
"chromatic": "dotenv cross-var npx chromatic --project-token=$CHROMATIC_PROJECT_TOKEN",
"install": "yarn lint:setup"
},
"eslintConfig": {
"extends": [
@ -147,6 +149,7 @@
"@types/scroll-into-view": "^1.16.0",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/utils": "^6.7.0",
"babel-plugin-named-exports-order": "^0.0.2",
"chromatic": "^6.18.0",
"concurrently": "^8.0.1",
@ -164,6 +167,7 @@
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-storybook": "^0.6.12",
"eslint-plugin-twenty": "file:../packages/eslint-plugin-twenty",
"eslint-plugin-twenty-ts": "file:../packages/eslint-plugin-twenty-ts",
"eslint-plugin-unused-imports": "^3.0.0",
"http-server": "^14.1.1",
"mock-apollo-client": "^1.2.1",

View File

@ -7,10 +7,9 @@ import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
import { CreateProfile } from '~/pages/auth/CreateProfile';
import { CreateWorkspace } from '~/pages/auth/CreateWorkspace';
import { SignInUp } from '~/pages/auth/SignInUp';
import { Verify } from '~/pages/auth/Verify';
import { VerifyEffect } from '~/pages/auth/VerifyEffect';
import { Companies } from '~/pages/companies/Companies';
import { CompanyShow } from '~/pages/companies/CompanyShow';
import { Impersonate } from '~/pages/impersonate/Impersonate';
import { Opportunities } from '~/pages/opportunities/Opportunities';
import { People } from '~/pages/people/People';
import { PersonShow } from '~/pages/people/PersonShow';
@ -19,8 +18,10 @@ import { SettingsProfile } from '~/pages/settings/SettingsProfile';
import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers';
import { Tasks } from '~/pages/tasks/Tasks';
import { AppInternalHooks } from '~/sync-hooks/AppInternalHooks';
import { CommandMenuEffect } from './effect-components/CommandMenuEffect';
import { GotoHotkeysEffect } from './effect-components/GotoHotkeysEffect';
import { ImpersonateEffect } from './pages/impersonate/ImpersonateEffect';
import { NotFound } from './pages/not-found/NotFound';
import { getPageTitleFromPath } from './utils/title-utils';
@ -34,10 +35,11 @@ export function App() {
return (
<>
<PageTitle title={pageTitle} />
<AppInternalHooks />
<GotoHotkeysEffect />
<CommandMenuEffect />
<DefaultLayout>
<Routes>
<Route path={AppPath.Verify} element={<Verify />} />
<Route path={AppPath.Verify} element={<VerifyEffect />} />
<Route path={AppPath.SignIn} element={<SignInUp />} />
<Route path={AppPath.SignUp} element={<SignInUp />} />
<Route path={AppPath.Invite} element={<SignInUp />} />
@ -49,7 +51,7 @@ export function App() {
<Route path={AppPath.CompaniesPage} element={<Companies />} />
<Route path={AppPath.CompanyShowPage} element={<CompanyShow />} />
<Route path={AppPath.TasksPage} element={<Tasks />} />
<Route path={AppPath.Impersonate} element={<Impersonate />} />
<Route path={AppPath.Impersonate} element={<ImpersonateEffect />} />
<Route path={AppPath.OpportunitiesPage} element={<Opportunities />} />
<Route

View File

@ -4,7 +4,7 @@ import { useSetRecoilState } from 'recoil';
import { commandMenuCommands } from '@/command-menu/constants/commandMenuCommands';
import { commandMenuCommandsState } from '@/command-menu/states/commandMenuCommandsState';
export function CommandMenuHook() {
export function CommandMenuEffect() {
const setCommands = useSetRecoilState(commandMenuCommandsState);
const commands = commandMenuCommands;

View File

@ -1,6 +1,6 @@
import { useGoToHotkeys } from '@/ui/utilities/hotkey/hooks/useGoToHotkeys';
export function GotoHotkeysHooks() {
export function GotoHotkeysEffect() {
useGoToHotkeys('p', '/people');
useGoToHotkeys('c', '/companies');
useGoToHotkeys('o', '/opportunities');

View File

@ -6,7 +6,7 @@ import { RecoilRoot } from 'recoil';
import { ApolloProvider } from '@/apollo/components/ApolloProvider';
import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
import { RecoilDebugObserver } from '@/debug/components/RecoilDebugObserver';
import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver';
import { DialogProvider } from '@/ui/dialog/components/DialogProvider';
import { SnackBarProvider } from '@/ui/snack-bar/components/SnackBarProvider';
import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
@ -15,7 +15,7 @@ import { UserProvider } from '@/users/components/UserProvider';
import '@emotion/react';
import { PageChangeEffect } from './sync-hooks/PageChangeEffect';
import { PageChangeEffect } from './effect-components/PageChangeEffect';
import { App } from './App';
import './index.css';
@ -27,7 +27,7 @@ const root = ReactDOM.createRoot(
root.render(
<RecoilRoot>
<RecoilDebugObserver />
<RecoilDebugObserverEffect />
<BrowserRouter>
<ApolloProvider>
<HelmetProvider>

View File

@ -6,7 +6,7 @@ import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { CompanyBoard } from '../board/components/CompanyBoard';
import { HooksCompanyBoard } from '../components/HooksCompanyBoard';
import { HooksCompanyBoardEffect } from '../components/HooksCompanyBoardEffect';
import { CompanyBoardRecoilScopeContext } from '../states/recoil-scope-contexts/CompanyBoardRecoilScopeContext';
const meta: Meta<typeof CompanyBoard> = {
@ -15,7 +15,7 @@ const meta: Meta<typeof CompanyBoard> = {
decorators: [
(Story) => (
<RecoilScope SpecificContext={CompanyBoardRecoilScopeContext}>
<HooksCompanyBoard />
<HooksCompanyBoardEffect />
<MemoryRouter>
<Story />
</MemoryRouter>

View File

@ -14,7 +14,7 @@ import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/Componen
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedPipelineProgressData } from '~/testing/mock-data/pipeline-progress';
import { HooksCompanyBoard } from '../components/HooksCompanyBoard';
import { HooksCompanyBoardEffect } from '../components/HooksCompanyBoardEffect';
import { CompanyBoardRecoilScopeContext } from '../states/recoil-scope-contexts/CompanyBoardRecoilScopeContext';
const meta: Meta<typeof CompanyBoardCard> = {
@ -33,7 +33,7 @@ const meta: Meta<typeof CompanyBoardCard> = {
return (
<>
<HooksCompanyBoard />
<HooksCompanyBoardEffect />
<RecoilScope SpecificContext={BoardColumnRecoilScopeContext}>
<BoardCardIdContext.Provider
value={mockedPipelineProgressData[1].id}

View File

@ -9,7 +9,7 @@ import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext';
import { useBoardViews } from '@/views/hooks/useBoardViews';
import { opportunitiesBoardOptions } from '~/pages/opportunities/opportunitiesBoardOptions';
import { HooksCompanyBoard } from '../../components/HooksCompanyBoard';
import { HooksCompanyBoardEffect } from '../../components/HooksCompanyBoardEffect';
import { CompanyBoardRecoilScopeContext } from '../../states/recoil-scope-contexts/CompanyBoardRecoilScopeContext';
type CompanyBoardProps = Pick<
@ -31,7 +31,7 @@ export const CompanyBoard = ({
return (
<>
<HooksCompanyBoard />
<HooksCompanyBoardEffect />
<ViewBarContext.Provider
value={{
defaultViewName: 'All Opportunities',

View File

@ -24,7 +24,7 @@ import { useUpdateCompanyBoardCardIds } from '../hooks/useUpdateBoardCardIds';
import { useUpdateCompanyBoard } from '../hooks/useUpdateCompanyBoardColumns';
import { CompanyBoardRecoilScopeContext } from '../states/recoil-scope-contexts/CompanyBoardRecoilScopeContext';
export function HooksCompanyBoard() {
export function HooksCompanyBoardEffect() {
const [, setAvailableFilters] = useRecoilScopedState(
availableFiltersScopedState,
CompanyBoardRecoilScopeContext,

View File

@ -9,7 +9,7 @@ import { companiesAvailableColumnDefinitions } from '../../constants/companiesAv
import { mockedCompaniesData } from './companies-mock-data';
export function CompanyTableMockData() {
export function CompanyTableMockDataEffect() {
const [, setTableColumns] = useRecoilScopedState(
tableColumnsScopedState,
TableRecoilScopeContext,

View File

@ -2,12 +2,12 @@ import { EntityTable } from '@/ui/table/components/EntityTable';
import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext';
import { useUpdateOneCompanyMutation } from '~/generated/graphql';
import { CompanyTableMockData } from './CompanyTableMockData';
import { CompanyTableMockDataEffect } from './CompanyTableMockDataEffect';
export function CompanyTableMockMode() {
return (
<>
<CompanyTableMockData />
<CompanyTableMockDataEffect />
<ViewBarContext.Provider value={{ defaultViewName: 'All Companies' }}>
<EntityTable updateEntityMutation={[useUpdateOneCompanyMutation()]} />
</ViewBarContext.Provider>

View File

@ -14,7 +14,7 @@ const formatTitle = (stateName: string) => {
return [parts.join(' '), ...headerCss];
};
export function RecoilDebugObserver() {
export function RecoilDebugObserverEffect() {
const snapshot = useRecoilSnapshot();
const isDebugMode = useRecoilValue(isDebugModeState);

View File

@ -6,7 +6,7 @@ import {
import { useSetPeopleEntityTable } from '../hooks/useSetPeopleEntityTable';
export function PeopleEntityTableData({
export function PeopleEntityTableDataEffect({
orderBy = [
{
createdAt: SortOrder.Desc,

View File

@ -5,7 +5,7 @@ type OwnProps = {
onAddButtonClick?: () => void;
};
export function PageHotkeys({ onAddButtonClick }: OwnProps) {
export function PageHotkeysEffect({ onAddButtonClick }: OwnProps) {
useScopedHotkeys('c', () => onAddButtonClick?.(), TableHotkeyScope.Table, [
onAddButtonClick,
]);

View File

@ -7,7 +7,7 @@ import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { AppPath } from '../../modules/types/AppPath';
import { isNonEmptyString } from '../../utils/isNonEmptyString';
export function Verify() {
export function VerifyEffect() {
const [searchParams] = useSearchParams();
const loginToken = searchParams.get('loginToken');

View File

@ -12,7 +12,7 @@ import { PageAddButton } from '@/ui/layout/components/PageAddButton';
import { PageBody } from '@/ui/layout/components/PageBody';
import { PageContainer } from '@/ui/layout/components/PageContainer';
import { PageHeader } from '@/ui/layout/components/PageHeader';
import { PageHotkeys } from '@/ui/layout/components/PageHotkeys';
import { PageHotkeysEffect } from '@/ui/layout/components/PageHotkeysEffect';
import { EntityTableActionBar } from '@/ui/table/action-bar/components/EntityTableActionBar';
import { EntityTableContextMenu } from '@/ui/table/context-menu/components/EntityTableContextMenu';
import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem';
@ -59,7 +59,7 @@ export function Companies() {
<PageContainer>
<PageHeader title="Companies" Icon={IconBuildingSkyscraper}>
<RecoilScope SpecificContext={DropdownRecoilScopeContext}>
<PageHotkeys onAddButtonClick={handleAddButtonClick} />
<PageHotkeysEffect onAddButtonClick={handleAddButtonClick} />
<PageAddButton onClick={handleAddButtonClick} />
</RecoilScope>
</PageHeader>

View File

@ -10,7 +10,7 @@ import { useImpersonateMutation } from '~/generated/graphql';
import { AppPath } from '../../modules/types/AppPath';
import { isNonEmptyString } from '../../utils/isNonEmptyString';
export function Impersonate() {
export function ImpersonateEffect() {
const navigate = useNavigate();
const { userId } = useParams();

View File

@ -10,7 +10,7 @@ import { PageAddButton } from '@/ui/layout/components/PageAddButton';
import { PageBody } from '@/ui/layout/components/PageBody';
import { PageContainer } from '@/ui/layout/components/PageContainer';
import { PageHeader } from '@/ui/layout/components/PageHeader';
import { PageHotkeys } from '@/ui/layout/components/PageHotkeys';
import { PageHotkeysEffect } from '@/ui/layout/components/PageHotkeysEffect';
import { EntityTableActionBar } from '@/ui/table/action-bar/components/EntityTableActionBar';
import { EntityTableContextMenu } from '@/ui/table/context-menu/components/EntityTableContextMenu';
import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem';
@ -55,7 +55,7 @@ export function People() {
<PageContainer>
<PageHeader title="People" Icon={IconUser}>
<RecoilScope SpecificContext={DropdownRecoilScopeContext}>
<PageHotkeys onAddButtonClick={handleAddButtonClick} />
<PageHotkeysEffect onAddButtonClick={handleAddButtonClick} />
<PageAddButton onClick={handleAddButtonClick} />
</RecoilScope>
</PageHeader>

View File

@ -1,11 +0,0 @@
import { CommandMenuHook } from './CommandMenuHook';
import { GotoHotkeysHooks } from './GotoHotkeysHooks';
export function AppInternalHooks() {
return (
<>
<GotoHotkeysHooks />
<CommandMenuHook />
</>
);
}

View File

@ -3,7 +3,7 @@ import { useEffect } from 'react';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
export function InitializeHotkeyStorybookHook() {
export function InitializeHotkeyStorybookHookEffect() {
const setHotkeyScope = useSetHotkeyScope();
useEffect(() => {

View File

@ -2,13 +2,13 @@ import { ApolloProvider } from '@apollo/client';
import { Decorator } from '@storybook/react';
import { RecoilRoot } from 'recoil';
import { InitializeHotkeyStorybookHook } from '../InitializeHotkeyStorybookHook';
import { InitializeHotkeyStorybookHookEffect } from '../InitializeHotkeyStorybookHook';
import { mockedClient } from '../mockedClient';
export const RootDecorator: Decorator = (Story) => (
<RecoilRoot>
<ApolloProvider client={mockedClient}>
<InitializeHotkeyStorybookHook />
<InitializeHotkeyStorybookHookEffect />
<Story />
</ApolloProvider>
</RecoilRoot>

View File

@ -1851,7 +1851,7 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d"
integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==
"@eslint-community/eslint-utils@^4.2.0":
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
@ -5462,7 +5462,7 @@
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138"
integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==
"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
version "7.0.12"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
@ -5673,6 +5673,11 @@
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a"
integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==
"@types/semver@^7.5.0":
version "7.5.2"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.2.tgz#31f6eec1ed7ec23f4f05608d3a2d381df041f564"
integrity sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==
"@types/send@*":
version "0.17.1"
resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301"
@ -5827,6 +5832,14 @@
"@typescript-eslint/types" "5.62.0"
"@typescript-eslint/visitor-keys" "5.62.0"
"@typescript-eslint/scope-manager@6.7.0":
version "6.7.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.7.0.tgz#6b3c22187976e2bf5ed0dc0d9095f1f2cbd1d106"
integrity sha512-lAT1Uau20lQyjoLUQ5FUMSX/dS07qux9rYd5FGzKz/Kf8W8ccuvMyldb8hadHdK/qOI7aikvQWqulnEq2nCEYA==
dependencies:
"@typescript-eslint/types" "6.7.0"
"@typescript-eslint/visitor-keys" "6.7.0"
"@typescript-eslint/type-utils@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a"
@ -5842,6 +5855,11 @@
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f"
integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==
"@typescript-eslint/types@6.7.0":
version "6.7.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.7.0.tgz#8de8ba9cafadc38e89003fe303e219c9250089ae"
integrity sha512-ihPfvOp7pOcN/ysoj0RpBPOx3HQTJTrIN8UZK+WFd3/iDeFHHqeyYxa4hQk4rMhsz9H9mXpR61IzwlBVGXtl9Q==
"@typescript-eslint/typescript-estree@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b"
@ -5855,6 +5873,19 @@
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/typescript-estree@6.7.0":
version "6.7.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.0.tgz#20ce2801733bd46f02cc0f141f5b63fbbf2afb63"
integrity sha512-dPvkXj3n6e9yd/0LfojNU8VMUGHWiLuBZvbM6V6QYD+2qxqInE7J+J/ieY2iGwR9ivf/R/haWGkIj04WVUeiSQ==
dependencies:
"@typescript-eslint/types" "6.7.0"
"@typescript-eslint/visitor-keys" "6.7.0"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.5.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/utils@5.62.0", "@typescript-eslint/utils@^5.45.0", "@typescript-eslint/utils@^5.58.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86"
@ -5869,6 +5900,19 @@
eslint-scope "^5.1.1"
semver "^7.3.7"
"@typescript-eslint/utils@^6.7.0":
version "6.7.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.7.0.tgz#61b6f1f1b82ad529abfcee074d21764e880886fb"
integrity sha512-MfCq3cM0vh2slSikQYqK2Gq52gvOhe57vD2RM3V4gQRZYX4rDPnKLu5p6cm89+LJiGlwEXU8hkYxhqqEC/V3qA==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
"@types/json-schema" "^7.0.12"
"@types/semver" "^7.5.0"
"@typescript-eslint/scope-manager" "6.7.0"
"@typescript-eslint/types" "6.7.0"
"@typescript-eslint/typescript-estree" "6.7.0"
semver "^7.5.4"
"@typescript-eslint/visitor-keys@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e"
@ -5877,6 +5921,14 @@
"@typescript-eslint/types" "5.62.0"
eslint-visitor-keys "^3.3.0"
"@typescript-eslint/visitor-keys@6.7.0":
version "6.7.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.0.tgz#34140ac76dfb6316d17012e4469acf3366ad3f44"
integrity sha512-/C1RVgKFDmGMcVGeD8HjKv2bd72oI1KxQDeY8uc66gw9R0OK0eMq48cA+jv9/2Ag6cdrsUGySm1yzYmfz0hxwQ==
dependencies:
"@typescript-eslint/types" "6.7.0"
eslint-visitor-keys "^3.4.1"
"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24"
@ -9772,6 +9824,9 @@ eslint-plugin-testing-library@^5.0.1:
dependencies:
"@typescript-eslint/utils" "^5.58.0"
"eslint-plugin-twenty-ts@file:../packages/eslint-plugin-twenty-ts":
version "1.0.1"
"eslint-plugin-twenty@file:../packages/eslint-plugin-twenty":
version "0.0.2"
dependencies:
@ -17078,7 +17133,7 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.0.0, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3:
semver@^7.0.0, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
@ -18211,6 +18266,11 @@ tryer@^1.0.1:
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==
ts-api-utils@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331"
integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==
ts-dedent@^2.0.0, ts-dedent@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5"

View File

@ -0,0 +1 @@
dist/

View File

@ -0,0 +1,5 @@
module.exports = {
rules: {
"effect-components": require("./src/rules/effect-components"),
},
};

View File

@ -0,0 +1,7 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
"moduleDirectories": ["node_modules"]
};

View File

@ -0,0 +1,35 @@
{
"name": "eslint-plugin-twenty-ts",
"version": "1.0.1",
"description": "",
"main": "dist/index.js",
"files": [
"dist",
"src"
],
"scripts": {
"test": "jest",
"build": "rimraf ./dist && tsc --outDir ./dist"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"@types/jest": "^29.5.4",
"@typescript-eslint/eslint-plugin": "^6.7.0",
"@typescript-eslint/parser": "^6.7.0",
"@typescript-eslint/rule-tester": "^6.7.0",
"@typescript-eslint/utils": "^6.7.0",
"eslint": "^8.49.0",
"eslint-config-prettier": "^9.0.0",
"eslint-config-standard-with-typescript": "^39.0.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^28.1.3",
"prettier": "^3.0.3",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
}
}

View File

@ -0,0 +1,114 @@
import { TSESTree, ESLintUtils } from "@typescript-eslint/utils";
const createRule = ESLintUtils.RuleCreator((name) => `https://docs.twenty.com`);
function checkIsPascalCase(input: string): boolean {
const pascalCaseRegex = /^(?:\p{Uppercase_Letter}\p{Letter}*)+$/u;
return pascalCaseRegex.test(input);
}
const effectComponentsRule = createRule({
create(context) {
const checkThatNodeIsEffectComponent = (node: TSESTree.FunctionDeclaration | TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression) => {
const isPascalCase = checkIsPascalCase(node.id?.name ?? "");
if(!isPascalCase) {
return;
}
const isReturningFragmentOrNull = (
// Direct return of JSX fragment, e.g., () => <></>
(node.body.type === 'JSXFragment' && node.body.children.length === 0) ||
// Direct return of null, e.g., () => null
(node.body.type === 'Literal' && node.body.value === null) ||
// Return JSX fragment or null from block
(node.body.type === 'BlockStatement' &&
node.body.body.some(statement =>
statement.type === 'ReturnStatement' &&
(
// Empty JSX fragment return, e.g., return <></>;
(statement.argument?.type === 'JSXFragment' && statement.argument.children.length === 0) ||
// Empty React.Fragment return, e.g., return <React.Fragment></React.Fragment>;
(statement.argument?.type === 'JSXElement' &&
statement.argument.openingElement.name.type === 'JSXIdentifier' &&
statement.argument.openingElement.name.name === 'React.Fragment' &&
statement.argument.children.length === 0) ||
// Literal null return, e.g., return null;
(statement.argument?.type === 'Literal' && statement.argument.value === null)
)
))
);
const hasEffectSuffix = node.id?.name.endsWith("Effect");
const hasEffectSuffixButIsNotEffectComponent = hasEffectSuffix && !isReturningFragmentOrNull
const isEffectComponentButDoesNotHaveEffectSuffix = !hasEffectSuffix && isReturningFragmentOrNull;
if(isEffectComponentButDoesNotHaveEffectSuffix) {
context.report({
node,
messageId: "effectSuffix",
data: {
componentName: node.id?.name,
},
fix(fixer) {
if (node.id) {
return fixer.replaceText(
node.id,
node.id?.name + "Effect",
);
}
return null;
},
});
} else if(hasEffectSuffixButIsNotEffectComponent) {
context.report({
node,
messageId: "noEffectSuffix",
data: {
componentName: node.id?.name,
},
fix(fixer) {
if (node.id) {
return fixer.replaceText(
node.id,
node.id?.name.replace("Effect", ""),
);
}
return null;
},
});
}
}
return {
ArrowFunctionExpression: checkThatNodeIsEffectComponent,
FunctionDeclaration: checkThatNodeIsEffectComponent,
FunctionExpression: checkThatNodeIsEffectComponent,
};
},
name: "effect-components",
meta: {
docs: {
description:
"Effect components should end with the Effect suffix. This rule checks only components that are in PascalCase and that return a JSX fragment or null. Any renderProps or camelCase components are ignored.",
},
messages: {
effectSuffix:
"Effect component {{ componentName }} should end with the Effect suffix.",
noEffectSuffix:
"Component {{ componentName }} shouldn't end with the Effect suffix because it doesn't return a JSX fragment or null.",
},
type: "suggestion",
schema: [],
fixable: "code",
},
defaultOptions: [],
});
module.exports = effectComponentsRule;
export default effectComponentsRule;

View File

@ -0,0 +1,97 @@
import { RuleTester } from "@typescript-eslint/rule-tester";
import effectComponentsRule from "../rules/effect-components";
const ruleTester = new RuleTester({
parser: "@typescript-eslint/parser",
parserOptions: {
project: "./tsconfig.json",
tsconfigRootDir: __dirname,
ecmaFeatures: {
jsx: true,
},
},
});
ruleTester.run("effect-components", effectComponentsRule, {
valid: [
{
code: `function TestComponentEffect() {
return <></>;
}`,
},
{
code: `function TestComponent() {
return <div></div>;
}`,
},
{
code: `export function useUpdateEffect() {
return null;
}`,
},
{
code: `export function useUpdateEffect() {
return <></>;
}`,
},
{
code: `function TestComponent() {
return <><div></div></>;
}`,
},
{
code: `function TestComponentEffect() {
return null;
}`,
},
{
code: `function TestComponentEffect() {
useEffect(() => {}, []);
return null;
}`,
},
{
code: `function TestComponentEffect() {
useEffect(() => {}, []);
return <></>;
}`,
},
{
code: `const TestComponentEffect = () => {
useEffect(() => {}, []);
return <></>;
}`,
},
{
code: `const TestComponentEffect = () => {
useEffect(() => {}, []);
return null;
}`,
},
],
invalid: [
{
code: "function TestComponent() { return <></>; }",
output: 'function TestComponentEffect() { return <></>; }',
errors: [
{
messageId: "effectSuffix",
},
],
},
{
code: "function TestComponentEffect() { return <><div></div></>; }",
output: 'function TestComponent() { return <><div></div></>; }',
errors: [
{
messageId: "noEffectSuffix",
},
],
},
],
});

View File

@ -0,0 +1 @@
// Required by typescript-eslint https://typescript-eslint.io/packages/rule-tester#type-aware-testing

View File

@ -0,0 +1 @@
// Required by typescript-eslint https://typescript-eslint.io/packages/rule-tester#type-aware-testing

View File

@ -0,0 +1,6 @@
{
"compilerOptions": {
"strict": true
},
"include": ["./file.ts", "./react.tsx"]
}

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and in clude compatible library declarations. */
"module": "Node16", /* Specify what module code is generated. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
"moduleResolution": "Node16",
}
}

File diff suppressed because it is too large Load Diff