mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-24 06:48:42 +03:00
POC: chore: use Nx workspace lint rules (#3163)
* chore: use Nx workspace lint rules Closes #3162 * Fix lint * Fix lint on BE * Fix tests --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
parent
1924962e8c
commit
8483cf0b4b
99
.eslintrc.js
Normal file
99
.eslintrc.js
Normal file
@ -0,0 +1,99 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['plugin:prettier/recommended'],
|
||||
plugins: ['@nx', 'prefer-arrow', 'simple-import-sort', 'unused-imports'],
|
||||
rules: {
|
||||
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
|
||||
'no-console': ['warn', { allow: ['group', 'groupCollapsed', 'groupEnd'] }],
|
||||
'no-unused-vars': 'off',
|
||||
'no-control-regex': 0,
|
||||
'no-undef': 'off',
|
||||
|
||||
'@nx/enforce-module-boundaries': [
|
||||
'error',
|
||||
{
|
||||
enforceBuildableLibDependency: true,
|
||||
allow: [],
|
||||
depConstraints: [
|
||||
{
|
||||
sourceTag: '*',
|
||||
onlyDependOnLibsWithTags: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
'prefer-arrow/prefer-arrow-functions': [
|
||||
'error',
|
||||
{
|
||||
disallowPrototype: true,
|
||||
singleReturnOnly: false,
|
||||
classPropertiesAllowed: false,
|
||||
},
|
||||
],
|
||||
|
||||
'simple-import-sort/imports': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
['^react', '^@?\\w'],
|
||||
['^(@|~)(/.*|$)'],
|
||||
['^\\u0000'],
|
||||
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
|
||||
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
|
||||
['^.+\\.?(css)$'],
|
||||
],
|
||||
},
|
||||
],
|
||||
'simple-import-sort/exports': 'error',
|
||||
|
||||
'unused-imports/no-unused-imports': 'warn',
|
||||
'unused-imports/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
vars: 'all',
|
||||
varsIgnorePattern: '^_',
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
extends: ['plugin:@nx/typescript'],
|
||||
rules: {
|
||||
'@typescript-eslint/ban-ts-comment': 'error',
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
vars: 'all',
|
||||
varsIgnorePattern: '^_',
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{ prefer: 'no-type-imports' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.js', '*.jsx'],
|
||||
extends: ['plugin:@nx/javascript'],
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: ['*.spec.@(ts|tsx|js|jsx)', '*.test.@(ts|tsx|js|jsx)'],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
rules: {},
|
||||
},
|
||||
],
|
||||
};
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -16,7 +16,6 @@
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
coverage
|
||||
.vercel
|
||||
|
||||
# yarn.lock should be managed at the root level only (nx monorepo)
|
||||
**/yarn.lock
|
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
||||
# Add files here to ignore them from prettier formatting
|
||||
/dist
|
||||
/coverage
|
||||
/.nx/cache
|
20
.vscode/extensions.json
vendored
20
.vscode/extensions.json
vendored
@ -1,17 +1,19 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-vscode-remote.remote-containers",
|
||||
"styled-components.vscode-styled-components",
|
||||
"arcanis.vscode-zipfs",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"ms-vscode.makefile-tools",
|
||||
"esbenp.prettier-vscode",
|
||||
"GraphQL.vscode-graphql",
|
||||
"yoavbls.pretty-ts-errors",
|
||||
"graphql.vscode-graphql-syntax",
|
||||
"graphql.vscode-graphql",
|
||||
"figma.figma-vscode-extension",
|
||||
"firsttris.vscode-jest-runner",
|
||||
"graphql.vscode-graphql-syntax",
|
||||
"GraphQL.vscode-graphql",
|
||||
"graphql.vscode-graphql",
|
||||
"ms-vscode-remote.remote-containers",
|
||||
"ms-vscode.makefile-tools",
|
||||
"nrwl.angular-console",
|
||||
"styled-components.vscode-styled-components",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"xyc.vscode-mdx-preview",
|
||||
"arcanis.vscode-zipfs"
|
||||
"yoavbls.pretty-ts-errors"
|
||||
]
|
||||
}
|
||||
|
8
.vscode/twenty.code-workspace
vendored
8
.vscode/twenty.code-workspace
vendored
@ -4,10 +4,6 @@
|
||||
"name": "ROOT",
|
||||
"path": "../"
|
||||
},
|
||||
{
|
||||
"name": "packages/eslint-plugin-twenty",
|
||||
"path": "../packages/eslint-plugin-twenty"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-docker",
|
||||
"path": "../packages/twenty-docker"
|
||||
@ -36,6 +32,10 @@
|
||||
"name": "packages/twenty-zapier",
|
||||
"path": "../packages/twenty-zapier"
|
||||
},
|
||||
{
|
||||
"name": "tools/eslint-rules",
|
||||
"path": "../tools/eslint-rules"
|
||||
},
|
||||
],
|
||||
"settings": {
|
||||
"files.exclude": {
|
||||
|
5
jest.config.js
Normal file
5
jest.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
import { getJestProjects } from '@nx/jest';
|
||||
|
||||
export default {
|
||||
projects: getJestProjects(),
|
||||
};
|
3
jest.preset.js
Normal file
3
jest.preset.js
Normal file
@ -0,0 +1,3 @@
|
||||
const nxPreset = require('@nx/jest/preset').default;
|
||||
|
||||
module.exports = { ...nxPreset };
|
24
nx.json
24
nx.json
@ -2,7 +2,9 @@
|
||||
"targetDefaults": {
|
||||
"build": {
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"]
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"lint": {
|
||||
"cache": true
|
||||
@ -12,12 +14,30 @@
|
||||
},
|
||||
"e2e": {
|
||||
"cache": true
|
||||
},
|
||||
"@nx/jest:jest": {
|
||||
"cache": true,
|
||||
"inputs": [
|
||||
"default",
|
||||
"^default",
|
||||
"{workspaceRoot}/jest.preset.js"
|
||||
],
|
||||
"options": {
|
||||
"passWithNoTests": true
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"ci": true,
|
||||
"codeCoverage": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"version": "17.2.0"
|
||||
"version": "17.2.7"
|
||||
},
|
||||
"affected": {
|
||||
"defaultBase": "main"
|
||||
}
|
||||
}
|
||||
|
||||
|
85
package.json
85
package.json
@ -10,20 +10,95 @@
|
||||
"license": "AGPL-3.0",
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/eslint-plugin-twenty",
|
||||
"packages/twenty-front",
|
||||
"packages/twenty-docs",
|
||||
"packages/twenty-server",
|
||||
"packages/twenty-utils",
|
||||
"packages/twenty-zapier",
|
||||
"packages/twenty-website"
|
||||
"packages/twenty-website",
|
||||
"tools/eslint-rules"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"nx": "17.2.0",
|
||||
"typescript": "^5.3.3"
|
||||
"@graphql-codegen/cli": "^3.3.1",
|
||||
"@graphql-codegen/client-preset": "^4.1.0",
|
||||
"@graphql-codegen/typescript": "^3.0.4",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.4",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@nx/eslint": "17.2.7",
|
||||
"@nx/eslint-plugin": "17.2.7",
|
||||
"@nx/jest": "17.2.7",
|
||||
"@nx/js": "17.2.7",
|
||||
"@storybook/addon-actions": "^7.6.3",
|
||||
"@storybook/addon-coverage": "^1.0.0",
|
||||
"@storybook/addon-essentials": "^7.6.3",
|
||||
"@storybook/addon-interactions": "^7.6.3",
|
||||
"@storybook/addon-links": "^7.6.3",
|
||||
"@storybook/addon-onboarding": "^1.0.9",
|
||||
"@storybook/addon-themes": "^7.6.3",
|
||||
"@storybook/blocks": "^7.6.3",
|
||||
"@storybook/react": "^7.6.3",
|
||||
"@storybook/react-vite": "^7.6.3",
|
||||
"@storybook/test": "^7.6.3",
|
||||
"@storybook/test-runner": "^0.16.0",
|
||||
"@stylistic/eslint-plugin": "^1.5.0",
|
||||
"@swc-node/register": "~1.6.7",
|
||||
"@swc/core": "~1.3.100",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@types/apollo-upload-client": "^17.0.2",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/lodash.camelcase": "^4.3.7",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/lodash.kebabcase": "^4.1.7",
|
||||
"@types/lodash.snakecase": "^4.1.9",
|
||||
"@types/luxon": "^3.3.0",
|
||||
"@types/node": "20.10.0",
|
||||
"@types/react": "^18.2.39",
|
||||
"@types/react-datepicker": "^4.11.2",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/scroll-into-view": "^1.16.0",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
"@typescript-eslint/parser": "^6.10.0",
|
||||
"@typescript-eslint/utils": "^6.9.1",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"chromatic": "^6.18.0",
|
||||
"concurrently": "^8.0.1",
|
||||
"cross-var": "^1.1.0",
|
||||
"dotenv-cli": "^7.2.1",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-prefer-arrow": "^1.2.3",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.4",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"eslint-plugin-storybook": "^0.6.15",
|
||||
"eslint-plugin-unused-imports": "^3.0.0",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"msw": "^2.0.11",
|
||||
"msw-storybook-addon": "2.0.0--canary.122.b3ed3b1.0",
|
||||
"nx": "17.2.7",
|
||||
"prettier": "3.1.0",
|
||||
"storybook": "^7.6.3",
|
||||
"storybook-addon-cookie": "^3.1.0",
|
||||
"storybook-addon-pseudo-states": "^2.1.2",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"patch-package": "^8.0.0"
|
||||
"patch-package": "^8.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,70 +0,0 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: [
|
||||
'@typescript-eslint/eslint-plugin',
|
||||
'unused-imports',
|
||||
'simple-import-sort',
|
||||
'prefer-arrow',
|
||||
],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
|
||||
rules: {
|
||||
'no-control-regex': 0,
|
||||
'simple-import-sort/imports': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
['^react', '^@?\\w'],
|
||||
['^(@|~)(/.*|$)'],
|
||||
['^\\u0000'],
|
||||
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
|
||||
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
|
||||
['^.+\\.?(css)$']
|
||||
]
|
||||
}
|
||||
],
|
||||
'prefer-arrow/prefer-arrow-functions': [
|
||||
'error',
|
||||
{
|
||||
"disallowPrototype": true,
|
||||
"singleReturnOnly": false,
|
||||
"classPropertiesAllowed": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
],
|
||||
ignorePatterns: ['.eslintrc.js', 'codegen.js', '**/generated/*', '*.config.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'simple-import-sort/imports': 'error',
|
||||
'simple-import-sort/exports': 'error',
|
||||
'func-style':['error', 'declaration', { 'allowArrowFunctions': true }],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "warn",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{ "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" }
|
||||
],
|
||||
"@typescript-eslint/consistent-type-imports": ["error", { "prefer": "no-type-imports" }],
|
||||
}
|
||||
};
|
1
packages/eslint-plugin-twenty/.gitignore
vendored
1
packages/eslint-plugin-twenty/.gitignore
vendored
@ -1 +0,0 @@
|
||||
dist/
|
Binary file not shown.
@ -1,11 +0,0 @@
|
||||
module.exports = {
|
||||
rules: {
|
||||
"effect-components": require("./src/rules/effect-components"),
|
||||
"no-hardcoded-colors": require("./src/rules/no-hardcoded-colors"),
|
||||
"matching-state-variable": require("./src/rules/matching-state-variable"),
|
||||
"sort-css-properties-alphabetically": require("./src/rules/sort-css-properties-alphabetically"),
|
||||
"styled-components-prefixed-with-styled": require("./src/rules/styled-components-prefixed-with-styled"),
|
||||
"component-props-naming": require("./src/rules/component-props-naming"),
|
||||
"no-state-useref": require("./src/rules/no-state-useref"),
|
||||
},
|
||||
};
|
@ -1,7 +0,0 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
|
||||
"moduleDirectories": ["node_modules"]
|
||||
};
|
@ -1,41 +0,0 @@
|
||||
{
|
||||
"name": "eslint-plugin-twenty",
|
||||
"version": "1.0.3",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"build": "rimraf ./dist && tsc --outDir ./dist",
|
||||
"build-pack": "yarn build && yarn pack -o eslint-plugin-twenty.tgz",
|
||||
"lint": "eslint src --max-warnings=0"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"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-prefer-arrow": "^1.2.3",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"eslint-plugin-unused-imports": "^3.0.0",
|
||||
"jest": "^28.1.3",
|
||||
"postcss": "^8.4.29",
|
||||
"prettier": "^3.0.3",
|
||||
"rimraf": "^5.0.5",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
import {
|
||||
AST_NODE_TYPES,
|
||||
ESLintUtils,
|
||||
TSESTree,
|
||||
} from "@typescript-eslint/utils";
|
||||
import { RuleContext } from "@typescript-eslint/utils/ts-eslint";
|
||||
|
||||
const createRule = ESLintUtils.RuleCreator(
|
||||
() => "https://docs.twenty.com/developer/frontend/style-guide#props",
|
||||
);
|
||||
|
||||
const checkPropsTypeName = (
|
||||
node: TSESTree.FunctionDeclaration | TSESTree.ArrowFunctionExpression,
|
||||
context: Readonly<RuleContext<"invalidPropsTypeName", never[]>>,
|
||||
functionName: string,
|
||||
) => {
|
||||
const expectedPropTypeName = `${functionName}Props`;
|
||||
|
||||
if (/^[A-Z]/.test(functionName)) {
|
||||
node.params.forEach((param) => {
|
||||
if (
|
||||
(param.type === AST_NODE_TYPES.ObjectPattern ||
|
||||
param.type === AST_NODE_TYPES.Identifier) &&
|
||||
param.typeAnnotation?.typeAnnotation?.type ===
|
||||
AST_NODE_TYPES.TSTypeReference &&
|
||||
param.typeAnnotation.typeAnnotation.typeName.type ===
|
||||
AST_NODE_TYPES.Identifier
|
||||
) {
|
||||
const { typeName } = param.typeAnnotation.typeAnnotation;
|
||||
const actualPropTypeName = typeName.name;
|
||||
if (actualPropTypeName !== expectedPropTypeName) {
|
||||
context.report({
|
||||
node: param,
|
||||
messageId: "invalidPropsTypeName",
|
||||
data: { expectedPropTypeName, actualPropTypeName },
|
||||
fix: (fixer) => fixer.replaceText(typeName, expectedPropTypeName),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const componentPropsNamingRule = createRule({
|
||||
create: (context) => {
|
||||
return {
|
||||
ArrowFunctionExpression: (node) => {
|
||||
if (
|
||||
node.parent.type === AST_NODE_TYPES.VariableDeclarator &&
|
||||
node.parent.id.type === AST_NODE_TYPES.Identifier
|
||||
) {
|
||||
const functionName = node.parent?.id?.name;
|
||||
|
||||
checkPropsTypeName(node, context, functionName);
|
||||
}
|
||||
},
|
||||
FunctionDeclaration: (node) => {
|
||||
if (node.id?.name) {
|
||||
const functionName = node.id.name;
|
||||
|
||||
checkPropsTypeName(node, context, functionName);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
name: "component-props-naming",
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Ensure component props follow naming convention",
|
||||
recommended: "recommended",
|
||||
},
|
||||
fixable: "code",
|
||||
schema: [],
|
||||
messages: {
|
||||
invalidPropsTypeName:
|
||||
"Expected prop type to be '{{ expectedPropTypeName }}' but found '{{ actualPropTypeName }}'",
|
||||
},
|
||||
},
|
||||
defaultOptions: [],
|
||||
});
|
||||
|
||||
module.exports = componentPropsNamingRule;
|
||||
export default componentPropsNamingRule;
|
@ -1,166 +0,0 @@
|
||||
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
|
||||
|
||||
const createRule = ESLintUtils.RuleCreator(() => `https://docs.twenty.com`);
|
||||
|
||||
const checkIsPascalCase = (input: string): boolean => {
|
||||
const pascalCaseRegex = /^[A-Z][a-zA-Z0-9_]*/g;
|
||||
|
||||
return pascalCaseRegex.test(input);
|
||||
};
|
||||
|
||||
type ComponentType =
|
||||
| TSESTree.FunctionDeclaration
|
||||
| TSESTree.ArrowFunctionExpression
|
||||
| TSESTree.FunctionExpression;
|
||||
|
||||
const effectComponentsRule = createRule({
|
||||
create: (context) => {
|
||||
const checkThatNodeIsEffectComponent = (node: ComponentType) => {
|
||||
let componentName = "";
|
||||
let identifierNode = node.id;
|
||||
|
||||
const isIdentifier = (
|
||||
node: TSESTree.Node | null,
|
||||
): node is TSESTree.Identifier =>
|
||||
node?.type === TSESTree.AST_NODE_TYPES.Identifier;
|
||||
const isVariableDeclarator = (
|
||||
node: TSESTree.Node,
|
||||
): node is TSESTree.VariableDeclarator =>
|
||||
node.type === TSESTree.AST_NODE_TYPES.VariableDeclarator;
|
||||
|
||||
const isArrowFunction = (
|
||||
node: TSESTree.Node,
|
||||
): node is TSESTree.ArrowFunctionExpression =>
|
||||
node.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression;
|
||||
const isFunctionDeclaration = (
|
||||
node: TSESTree.Node,
|
||||
): node is TSESTree.FunctionDeclaration =>
|
||||
node.type === TSESTree.AST_NODE_TYPES.FunctionDeclaration;
|
||||
const isFunctionExpression = (
|
||||
node: TSESTree.Node,
|
||||
): node is TSESTree.FunctionExpression =>
|
||||
node.type === TSESTree.AST_NODE_TYPES.FunctionExpression;
|
||||
|
||||
if (
|
||||
isArrowFunction(node) &&
|
||||
isVariableDeclarator(node.parent) &&
|
||||
isIdentifier(node.parent.id)
|
||||
) {
|
||||
componentName = node.parent.id.name;
|
||||
identifierNode = node.parent.id;
|
||||
} else if (isFunctionDeclaration(node) && isIdentifier(node.id)) {
|
||||
componentName = node.id.name;
|
||||
identifierNode = node.id;
|
||||
} else if (
|
||||
isFunctionExpression(node) &&
|
||||
isVariableDeclarator(node.parent) &&
|
||||
isIdentifier(node.parent.id)
|
||||
) {
|
||||
componentName = node.parent.id.name;
|
||||
identifierNode = node.parent.id;
|
||||
}
|
||||
|
||||
if (!checkIsPascalCase(componentName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isReturningEmptyFragmentOrNull =
|
||||
// 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 = componentName.endsWith("Effect");
|
||||
|
||||
const hasEffectSuffixButIsNotEffectComponent =
|
||||
hasEffectSuffix && !isReturningEmptyFragmentOrNull;
|
||||
const isEffectComponentButDoesNotHaveEffectSuffix =
|
||||
!hasEffectSuffix && isReturningEmptyFragmentOrNull;
|
||||
|
||||
if (isEffectComponentButDoesNotHaveEffectSuffix) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "effectSuffix",
|
||||
data: {
|
||||
componentName: componentName,
|
||||
},
|
||||
fix: (fixer) => {
|
||||
if (isArrowFunction(node))
|
||||
if (identifierNode) {
|
||||
return fixer.replaceText(
|
||||
identifierNode,
|
||||
componentName + "Effect",
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
} else if (hasEffectSuffixButIsNotEffectComponent) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noEffectSuffix",
|
||||
data: {
|
||||
componentName: componentName,
|
||||
},
|
||||
fix: (fixer) => {
|
||||
if (identifierNode) {
|
||||
return fixer.replaceText(
|
||||
identifierNode,
|
||||
componentName.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;
|
@ -1,68 +0,0 @@
|
||||
import { ESLintUtils } from "@typescript-eslint/utils";
|
||||
|
||||
const createRule = ESLintUtils.RuleCreator(() => `https://docs.twenty.com`);
|
||||
|
||||
const noStateUseRef = createRule({
|
||||
create: (context) => {
|
||||
return {
|
||||
CallExpression: (node) => {
|
||||
if (
|
||||
node.callee.type !== "Identifier" ||
|
||||
node.callee.name !== "useRef"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!node.typeArguments || !node.typeArguments.params?.length) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noStateUseRef",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const typeParam = node.typeArguments.params[0];
|
||||
|
||||
if (typeParam.type !== "TSTypeReference") {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noStateUseRef",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeParam.typeName.type !== "Identifier") {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noStateUseRef",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!typeParam.typeName.name.match(/^(HTML.*Element|Element)$/)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "test",
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
name: "no-state-useref",
|
||||
meta: {
|
||||
docs: {
|
||||
description: "Don't use useRef for state management",
|
||||
},
|
||||
messages: {
|
||||
test: "test",
|
||||
noStateUseRef:
|
||||
"Don't use useRef for state management. See https://docs.twenty.com/developer/frontend/best-practices#do-not-use-useref-to-store-state for more details.",
|
||||
},
|
||||
type: "suggestion",
|
||||
schema: [],
|
||||
},
|
||||
defaultOptions: [],
|
||||
});
|
||||
|
||||
module.exports = noStateUseRef;
|
||||
|
||||
export default noStateUseRef;
|
@ -1,298 +0,0 @@
|
||||
import { TSESTree } from "@typescript-eslint/utils";
|
||||
import { ESLintUtils } from "@typescript-eslint/utils";
|
||||
import {
|
||||
RuleFix,
|
||||
RuleFixer,
|
||||
SourceCode,
|
||||
} from "@typescript-eslint/utils/ts-eslint";
|
||||
|
||||
import postcss from "postcss";
|
||||
|
||||
const createRule = ESLintUtils.RuleCreator(() => `https://docs.twenty.com`);
|
||||
|
||||
interface loc {
|
||||
start: {
|
||||
line: number;
|
||||
column: number;
|
||||
};
|
||||
end: {
|
||||
line: number;
|
||||
column: number;
|
||||
};
|
||||
}
|
||||
|
||||
const isStyledTagname = (node: TSESTree.TaggedTemplateExpression): boolean => {
|
||||
const isMemberExpression = (
|
||||
node: TSESTree.Node,
|
||||
): node is TSESTree.MemberExpression =>
|
||||
node.type === TSESTree.AST_NODE_TYPES.MemberExpression;
|
||||
const isCallExpression = (
|
||||
node: TSESTree.Node,
|
||||
): node is TSESTree.CallExpression =>
|
||||
node.type === TSESTree.AST_NODE_TYPES.CallExpression;
|
||||
const isIdentifier = (
|
||||
node: TSESTree.Node | null,
|
||||
): node is TSESTree.Identifier =>
|
||||
node?.type === TSESTree.AST_NODE_TYPES.Identifier;
|
||||
|
||||
if (isIdentifier(node.tag)) {
|
||||
return node.tag.name === "css";
|
||||
}
|
||||
|
||||
if (isMemberExpression(node.tag) && isIdentifier(node.tag.object)) {
|
||||
return node.tag.object.name === "styled";
|
||||
}
|
||||
|
||||
if (isCallExpression(node.tag) && isIdentifier(node.tag.callee)) {
|
||||
return node.tag.callee.name === "styled";
|
||||
}
|
||||
|
||||
if (
|
||||
isCallExpression(node.tag) &&
|
||||
isMemberExpression(node.tag.callee) &&
|
||||
isIdentifier(node.tag.callee.object)
|
||||
) {
|
||||
return node.tag.callee.object.name === "styled";
|
||||
}
|
||||
|
||||
if (
|
||||
isCallExpression(node.tag) &&
|
||||
isMemberExpression(node.tag.callee) &&
|
||||
isIdentifier(node.tag.callee.object)
|
||||
) {
|
||||
return node.tag.callee.object.name === "styled";
|
||||
}
|
||||
|
||||
if (
|
||||
isCallExpression(node.tag) &&
|
||||
isMemberExpression(node.tag.callee) &&
|
||||
isMemberExpression(node.tag.callee.object) &&
|
||||
isIdentifier(node.tag.callee.object.object)
|
||||
) {
|
||||
return node.tag.callee.object.object.name === "styled";
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* An atomic rule is a rule without nested rules.
|
||||
*/
|
||||
const isValidAtomicRule = (
|
||||
rule: postcss.Rule,
|
||||
): { isValid: boolean; loc?: loc } => {
|
||||
const decls = rule.nodes.filter(
|
||||
(node) => node.type === "decl",
|
||||
) as unknown as postcss.Declaration[];
|
||||
if (decls.length < 0) {
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
for (let i = 1; i < decls.length; i++) {
|
||||
const current = decls[i].prop;
|
||||
const prev = decls[i - 1].prop;
|
||||
if (current < prev) {
|
||||
const loc = {
|
||||
start: {
|
||||
line: decls[i - 1].source!.start!.line,
|
||||
column: decls[i - 1].source!.start!.column - 1,
|
||||
},
|
||||
end: {
|
||||
line: decls[i].source!.end!.line,
|
||||
column: decls[i].source!.end!.column - 1,
|
||||
},
|
||||
};
|
||||
|
||||
return { isValid: false, loc };
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
};
|
||||
|
||||
const isValidRule = (rule: postcss.Rule): { isValid: boolean; loc?: loc } => {
|
||||
// check each rule recursively
|
||||
const { isValid, loc } = rule.nodes.reduce<{ isValid: boolean; loc?: loc }>(
|
||||
(map, node) => {
|
||||
return node.type === "rule" ? isValidRule(node) : map;
|
||||
},
|
||||
{ isValid: true },
|
||||
);
|
||||
|
||||
// if there is any invalid rule, return result
|
||||
if (!isValid) {
|
||||
return { isValid, loc };
|
||||
}
|
||||
|
||||
// check declarations
|
||||
return isValidAtomicRule(rule);
|
||||
};
|
||||
|
||||
const getNodeStyles = (node: TSESTree.TaggedTemplateExpression): string => {
|
||||
const [firstQuasi, ...quasis] = node.quasi.quasis;
|
||||
// remove line break added to the first quasi
|
||||
const lineBreakCount = node.quasi.loc.start.line - 1;
|
||||
let styles = `${"\n".repeat(lineBreakCount)}${" ".repeat(
|
||||
node.quasi.loc.start.column + 1,
|
||||
)}${firstQuasi.value.raw}`;
|
||||
|
||||
// replace expression by spaces and line breaks
|
||||
quasis.forEach(({ value, loc }, idx) => {
|
||||
const prevLoc = idx === 0 ? firstQuasi.loc : quasis[idx - 1].loc;
|
||||
const lineBreaksCount = loc.start.line - prevLoc.end.line;
|
||||
const spacesCount =
|
||||
loc.start.line === prevLoc.end.line
|
||||
? loc.start.column - prevLoc.end.column + 2
|
||||
: loc.start.column + 1;
|
||||
styles = `${styles}${" "}${"\n".repeat(lineBreaksCount)}${" ".repeat(
|
||||
spacesCount,
|
||||
)}${value.raw}`;
|
||||
});
|
||||
|
||||
return styles;
|
||||
};
|
||||
|
||||
const fix = ({
|
||||
rule,
|
||||
fixer,
|
||||
src,
|
||||
}: {
|
||||
rule: postcss.Rule;
|
||||
fixer: RuleFixer;
|
||||
src: SourceCode;
|
||||
}): RuleFix[] => {
|
||||
let fixings: RuleFix[] = [];
|
||||
|
||||
// concat fixings recursively
|
||||
rule.nodes.forEach((node) => {
|
||||
if (node.type === "rule") {
|
||||
fixings = [...fixings, ...fix({ rule: node, fixer, src })];
|
||||
}
|
||||
});
|
||||
|
||||
const declarations = rule.nodes.filter(
|
||||
(node) => node.type === "decl",
|
||||
) as unknown as postcss.Declaration[];
|
||||
const sortedDeclarations = sortDeclarations(declarations);
|
||||
|
||||
declarations.forEach((decl, idx) => {
|
||||
if (!areSameDeclarations(decl, sortedDeclarations[idx])) {
|
||||
try {
|
||||
const range = getDeclRange({ decl, src });
|
||||
const sortedDeclText = getDeclText({
|
||||
decl: sortedDeclarations[idx],
|
||||
src,
|
||||
});
|
||||
|
||||
fixings.push(fixer.removeRange([range.startIdx, range.endIdx + 1]));
|
||||
fixings.push(
|
||||
fixer.insertTextAfterRange(
|
||||
[range.startIdx, range.startIdx],
|
||||
sortedDeclText,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
return fixings;
|
||||
};
|
||||
|
||||
const areSameDeclarations = (
|
||||
a: postcss.ChildNode,
|
||||
b: postcss.ChildNode,
|
||||
): boolean =>
|
||||
a.source!.start!.line === b.source!.start!.line &&
|
||||
a.source!.start!.column === b.source!.start!.column;
|
||||
|
||||
const getDeclRange = ({
|
||||
decl,
|
||||
src,
|
||||
}: {
|
||||
decl: postcss.ChildNode;
|
||||
src: SourceCode;
|
||||
}): { startIdx: number; endIdx: number } => {
|
||||
const loc = {
|
||||
start: {
|
||||
line: decl.source!.start!.line,
|
||||
column: decl.source!.start!.column - 1,
|
||||
},
|
||||
end: {
|
||||
line: decl.source!.end!.line,
|
||||
column: decl.source!.end!.column - 1,
|
||||
},
|
||||
};
|
||||
|
||||
const startIdx = src.getIndexFromLoc(loc.start);
|
||||
const endIdx = src.getIndexFromLoc(loc.end);
|
||||
return { startIdx, endIdx };
|
||||
};
|
||||
|
||||
const getDeclText = ({
|
||||
decl,
|
||||
src,
|
||||
}: {
|
||||
decl: postcss.ChildNode;
|
||||
src: SourceCode;
|
||||
}) => {
|
||||
const { startIdx, endIdx } = getDeclRange({ decl, src });
|
||||
return src.getText().substring(startIdx, endIdx + 1);
|
||||
};
|
||||
|
||||
const sortDeclarations = (declarations: postcss.Declaration[]) =>
|
||||
declarations
|
||||
.slice()
|
||||
.sort((declA, declB) => (declA.prop > declB.prop ? 1 : -1));
|
||||
|
||||
const sortCssPropertiesAlphabeticallyRule = createRule({
|
||||
create: (context) => {
|
||||
return {
|
||||
TaggedTemplateExpression: (node: TSESTree.TaggedTemplateExpression) => {
|
||||
if (isStyledTagname(node)) {
|
||||
try {
|
||||
const root = postcss.parse(
|
||||
getNodeStyles(node),
|
||||
) as unknown as postcss.Rule;
|
||||
|
||||
const { isValid } = isValidRule(root);
|
||||
|
||||
if (!isValid) {
|
||||
return context.report({
|
||||
node,
|
||||
messageId: "sortCssPropertiesAlphabetically",
|
||||
fix: (fixer) =>
|
||||
fix({
|
||||
rule: root,
|
||||
fixer,
|
||||
src: context.getSourceCode(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
name: "sort-css-properties-alphabetically",
|
||||
meta: {
|
||||
docs: {
|
||||
description: "Styles are sorted alphabetically.",
|
||||
recommended: "recommended",
|
||||
},
|
||||
messages: {
|
||||
sortCssPropertiesAlphabetically:
|
||||
"Declarations should be sorted alphabetically.",
|
||||
},
|
||||
type: "suggestion",
|
||||
schema: [],
|
||||
fixable: "code",
|
||||
},
|
||||
defaultOptions: [],
|
||||
});
|
||||
|
||||
module.exports = sortCssPropertiesAlphabeticallyRule;
|
||||
|
||||
export default sortCssPropertiesAlphabeticallyRule;
|
@ -1,62 +0,0 @@
|
||||
import {
|
||||
AST_NODE_TYPES,
|
||||
ESLintUtils,
|
||||
TSESTree,
|
||||
} from "@typescript-eslint/utils";
|
||||
|
||||
const createRule = ESLintUtils.RuleCreator(() => `https://docs.twenty.com`);
|
||||
|
||||
const styledComponentsPrefixedWithStyledRule = createRule({
|
||||
create: (context) => {
|
||||
return {
|
||||
VariableDeclarator: (node: TSESTree.VariableDeclarator) => {
|
||||
const templateExpr = node.init;
|
||||
if (templateExpr?.type !== AST_NODE_TYPES.TaggedTemplateExpression) {
|
||||
return;
|
||||
}
|
||||
const tag = templateExpr.tag;
|
||||
const tagged =
|
||||
tag.type === AST_NODE_TYPES.MemberExpression
|
||||
? tag.object
|
||||
: tag.type === AST_NODE_TYPES.CallExpression
|
||||
? tag.callee
|
||||
: null;
|
||||
if (
|
||||
tagged?.type === AST_NODE_TYPES.Identifier &&
|
||||
tagged.name === "styled"
|
||||
) {
|
||||
const variable = node.id as TSESTree.Identifier;
|
||||
if (variable.name.startsWith("Styled")) {
|
||||
return;
|
||||
}
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noStyledPrefix",
|
||||
data: {
|
||||
componentName: variable.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
name: "styled-components-prefixed-with-styled",
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
docs: {
|
||||
description: "Warn when StyledComponents are not prefixed with Styled",
|
||||
recommended: "recommended",
|
||||
},
|
||||
messages: {
|
||||
noStyledPrefix:
|
||||
"{{componentName}} is a StyledComponent and is not prefixed with Styled.",
|
||||
},
|
||||
fixable: "code",
|
||||
schema: [],
|
||||
},
|
||||
defaultOptions: [],
|
||||
});
|
||||
|
||||
module.exports = styledComponentsPrefixedWithStyledRule;
|
||||
|
||||
export default styledComponentsPrefixedWithStyledRule;
|
@ -1,47 +0,0 @@
|
||||
import { RuleTester } from "@typescript-eslint/rule-tester";
|
||||
|
||||
import componentPropsNamingRule from "../rules/component-props-naming";
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run("component-props-naming", componentPropsNamingRule, {
|
||||
valid: [
|
||||
{
|
||||
code: "export const MyComponent= (props: MyComponentProps) => <div>{props.message}</div>;",
|
||||
},
|
||||
{
|
||||
code: "export const MyComponent = ({ message }: MyComponentProps) => <div>{message}</div>;",
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "export const MyComponent = (props: OwnProps) => <div>{props.message}</div>;",
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidPropsTypeName",
|
||||
},
|
||||
],
|
||||
output:
|
||||
"export const MyComponent = (props: MyComponentProps) => <div>{props.message}</div>;",
|
||||
},
|
||||
{
|
||||
code: "export const MyComponent = ({ message }: OwnProps) => <div>{message}</div>;",
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidPropsTypeName",
|
||||
},
|
||||
],
|
||||
output:
|
||||
"export const MyComponent = ({ message }: MyComponentProps) => <div>{message}</div>;",
|
||||
},
|
||||
],
|
||||
});
|
@ -1 +0,0 @@
|
||||
// Required by typescript-eslint https://typescript-eslint.io/packages/rule-tester#type-aware-testing
|
@ -1,185 +0,0 @@
|
||||
import { RuleTester } from "@typescript-eslint/rule-tester";
|
||||
|
||||
import matchingStateVariableRule from "../rules/matching-state-variable";
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run("matching-state-variable", matchingStateVariableRule, {
|
||||
valid: [
|
||||
{
|
||||
code: "const variable = useRecoilValue(variableState);",
|
||||
},
|
||||
{
|
||||
code: "const variable = useRecoilScopedValue(variableScopedState);",
|
||||
},
|
||||
{
|
||||
code: "const [variable, setVariable] = useRecoilState(variableScopedState);",
|
||||
},
|
||||
{
|
||||
code: "const [variable, setVariable] = useRecoilScopedState(variableScopedState);",
|
||||
},
|
||||
{
|
||||
code: "const [variable, setVariable] = useRecoilFamilyState(variableScopedState);",
|
||||
},
|
||||
{
|
||||
code: "const [variable, setVariable] = useRecoilScopedFamilyState(variableScopedState);",
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "const myValue = useRecoilValue(variableState);",
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidVariableName",
|
||||
},
|
||||
],
|
||||
output: "const variable = useRecoilValue(variableState);",
|
||||
},
|
||||
{
|
||||
code: "const myValue = useRecoilScopedValue(variableState);",
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidVariableName",
|
||||
},
|
||||
],
|
||||
output: "const variable = useRecoilScopedValue(variableState);",
|
||||
},
|
||||
|
||||
{
|
||||
code: "const [myValue, setMyValue] = useRecoilState(variableState);",
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidVariableName",
|
||||
},
|
||||
{
|
||||
messageId: "invalidSetterName",
|
||||
},
|
||||
],
|
||||
output: "const [variable, setVariable] = useRecoilState(variableState);",
|
||||
},
|
||||
{
|
||||
code: "const [myValue] = useRecoilState(variableState);",
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidVariableName",
|
||||
},
|
||||
],
|
||||
output: "const [variable] = useRecoilState(variableState);",
|
||||
},
|
||||
{
|
||||
code: "const [, setMyValue] = useRecoilState(variableState);",
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidSetterName",
|
||||
},
|
||||
],
|
||||
output: "const [, setVariable] = useRecoilState(variableState);",
|
||||
},
|
||||
|
||||
{
|
||||
code: "const [myValue, setMyValue] = useRecoilScopedState(variableState);",
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidVariableName",
|
||||
},
|
||||
{
|
||||
messageId: "invalidSetterName",
|
||||
},
|
||||
],
|
||||
output:
|
||||
"const [variable, setVariable] = useRecoilScopedState(variableState);",
|
||||
},
|
||||
{
|
||||
code: "const [myValue] = useRecoilScopedState(variableState);",
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidVariableName",
|
||||
},
|
||||
],
|
||||
output: "const [variable] = useRecoilScopedState(variableState);",
|
||||
},
|
||||
{
|
||||
code: "const [, setMyValue] = useRecoilScopedState(variableState);",
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidSetterName",
|
||||
},
|
||||
],
|
||||
output: "const [, setVariable] = useRecoilScopedState(variableState);",
|
||||
},
|
||||
|
||||
{
|
||||
code: "const [myValue, setMyValue] = useRecoilFamilyState(variableState);",
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidVariableName",
|
||||
},
|
||||
{
|
||||
messageId: "invalidSetterName",
|
||||
},
|
||||
],
|
||||
output:
|
||||
"const [variable, setVariable] = useRecoilFamilyState(variableState);",
|
||||
},
|
||||
{
|
||||
code: "const [myValue] = useRecoilFamilyState(variableState);",
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidVariableName",
|
||||
},
|
||||
],
|
||||
output: "const [variable] = useRecoilFamilyState(variableState);",
|
||||
},
|
||||
{
|
||||
code: "const [, setMyValue] = useRecoilFamilyState(variableState);",
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidSetterName",
|
||||
},
|
||||
],
|
||||
output: "const [, setVariable] = useRecoilFamilyState(variableState);",
|
||||
},
|
||||
|
||||
{
|
||||
code: "const [myValue, setMyValue] = useRecoilScopedFamilyState(variableState);",
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidVariableName",
|
||||
},
|
||||
{
|
||||
messageId: "invalidSetterName",
|
||||
},
|
||||
],
|
||||
output:
|
||||
"const [variable, setVariable] = useRecoilScopedFamilyState(variableState);",
|
||||
},
|
||||
{
|
||||
code: "const [myValue] = useRecoilScopedFamilyState(variableState);",
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidVariableName",
|
||||
},
|
||||
],
|
||||
output: "const [variable] = useRecoilScopedFamilyState(variableState);",
|
||||
},
|
||||
{
|
||||
code: "const [, setMyValue] = useRecoilScopedFamilyState(variableState);",
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidSetterName",
|
||||
},
|
||||
],
|
||||
output:
|
||||
"const [, setVariable] = useRecoilScopedFamilyState(variableState);",
|
||||
},
|
||||
],
|
||||
});
|
@ -1,62 +0,0 @@
|
||||
import { RuleTester } from "@typescript-eslint/rule-tester";
|
||||
|
||||
import noHardcodedColorsRule from "../rules/no-hardcoded-colors";
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run("no-hardcoded-colors", noHardcodedColorsRule, {
|
||||
valid: [
|
||||
{
|
||||
code: "const color = theme.background.secondary;",
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: 'const color = "rgb(154,205,50)";',
|
||||
errors: [
|
||||
{
|
||||
messageId: "hardcodedColor",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'const color = { test: "rgb(154,205,50)", test2: "#ADFF2F" }',
|
||||
errors: [
|
||||
{
|
||||
messageId: "hardcodedColor",
|
||||
},
|
||||
{
|
||||
messageId: "hardcodedColor",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "const color = { test: `rgb(${r},${g},${b})`, test2: `#ADFF${test}` }",
|
||||
errors: [
|
||||
{
|
||||
messageId: "hardcodedColor",
|
||||
},
|
||||
{
|
||||
messageId: "hardcodedColor",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'const color = "#ADFF2F";',
|
||||
errors: [
|
||||
{
|
||||
messageId: "hardcodedColor",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
@ -1,51 +0,0 @@
|
||||
import { RuleTester } from "@typescript-eslint/rule-tester";
|
||||
|
||||
import noStateUseRefRule from "../rules/no-state-useref";
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run("no-state-useref", noStateUseRefRule, {
|
||||
valid: [
|
||||
{
|
||||
code: "const scrollableRef = useRef<HTMLDivElement>(null);",
|
||||
},
|
||||
{
|
||||
code: "const ref = useRef<HTMLInputElement>(null);",
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "const ref = useRef(null);",
|
||||
errors: [
|
||||
{
|
||||
messageId: "noStateUseRef",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "const ref = useRef<Boolean>(null);",
|
||||
errors: [
|
||||
{
|
||||
messageId: "noStateUseRef",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "const ref = useRef<string>('');",
|
||||
errors: [
|
||||
{
|
||||
messageId: "noStateUseRef",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
@ -1 +0,0 @@
|
||||
// Required by typescript-eslint https://typescript-eslint.io/packages/rule-tester#type-aware-testing
|
@ -1,56 +0,0 @@
|
||||
import { RuleTester } from "@typescript-eslint/rule-tester";
|
||||
|
||||
import sortCssPropertiesAlphabeticallyRule from "../rules/sort-css-properties-alphabetically";
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run(
|
||||
"sort-css-properties-alphabetically",
|
||||
sortCssPropertiesAlphabeticallyRule,
|
||||
{
|
||||
valid: [
|
||||
{
|
||||
code: "const style = css`color: red;`;",
|
||||
},
|
||||
{
|
||||
code: "const style = css`background-color: $bgColor;color: red;`;",
|
||||
},
|
||||
{
|
||||
code: "const StyledComponent = styled.div`color: red;`;",
|
||||
},
|
||||
{
|
||||
code: "const StyledComponent = styled.div`background-color: $bgColor;color: red;`;",
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "const style = css`color: #FF0000;background-color: $bgColor`;",
|
||||
output: "const style = css`background-color: $bgColorcolor: #FF0000;`;",
|
||||
errors: [
|
||||
{
|
||||
messageId: "sortCssPropertiesAlphabetically",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "const StyledComponent = styled.div`color: #FF0000;background-color: $bgColor`;",
|
||||
output:
|
||||
"const StyledComponent = styled.div`background-color: $bgColorcolor: #FF0000;`;",
|
||||
errors: [
|
||||
{
|
||||
messageId: "sortCssPropertiesAlphabetically",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
@ -1,47 +0,0 @@
|
||||
import { RuleTester } from "@typescript-eslint/rule-tester";
|
||||
|
||||
import styledComponentsPrefixedWithStyledRule from "../rules/styled-components-prefixed-with-styled";
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run(
|
||||
"styled-components-prefixed-with-styled",
|
||||
styledComponentsPrefixedWithStyledRule,
|
||||
{
|
||||
valid: [
|
||||
{
|
||||
code: "const StyledButton = styled.button``;",
|
||||
},
|
||||
{
|
||||
code: "const StyledComponent = styled.div``;",
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "const Button = styled.button``;",
|
||||
errors: [
|
||||
{
|
||||
messageId: "noStyledPrefix",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "const Component = styled.div``;",
|
||||
errors: [
|
||||
{
|
||||
messageId: "noStyledPrefix",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
},
|
||||
"include": ["./file.ts", "./react.tsx"]
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
{
|
||||
"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",
|
||||
}
|
||||
}
|
@ -12,7 +12,6 @@ services:
|
||||
- ../../..:/app
|
||||
- twenty_dev_node_modules_root:/app/node_modules
|
||||
- twenty_dev_node_modules_docs:/app/packages/twenty-docs/node_modules
|
||||
- twenty_dev_node_modules_eslint:/app/packages/eslint-plugin-twenty/node_modules
|
||||
- twenty_dev_node_modules_front:/app/packages/twenty-front/node_modules
|
||||
- twenty_dev_node_modules_server:/app/packages/twenty-server/node_modules
|
||||
- twenty_dev_node_modules_website:/app/packages/twenty-website/node_modules
|
||||
|
@ -10,7 +10,7 @@ COPY ./package.json .
|
||||
COPY ./yarn.lock .
|
||||
COPY ./.yarnrc.yml .
|
||||
COPY ./.yarn/releases /app/.yarn/releases
|
||||
COPY ./packages/eslint-plugin-twenty /app/packages/eslint-plugin-twenty
|
||||
COPY ./tools/eslint-rules /app/tools/eslint-rules
|
||||
COPY ./packages/twenty-front /app/packages/twenty-front
|
||||
|
||||
RUN yarn
|
||||
|
@ -6,7 +6,7 @@ COPY ./package.json .
|
||||
COPY ./yarn.lock .
|
||||
COPY ./.yarnrc.yml .
|
||||
COPY ./.yarn/releases /app/.yarn/releases
|
||||
COPY ./packages/eslint-plugin-twenty /app/packages/eslint-plugin-twenty
|
||||
COPY ./tools/eslint-rules /app/tools/eslint-rules
|
||||
COPY ./packages/twenty-server /app/packages/twenty-server
|
||||
RUN yarn
|
||||
|
||||
|
@ -37,7 +37,7 @@ twenty
|
||||
└───twenty-front // contains the frontend code for the application
|
||||
└───twenty-server // contains the backend code for the application
|
||||
└───twenty-docker // contains docker configurations for development and production build
|
||||
└───many other packages your are invited to discover such as twenty-docs, twenty-ui, eslint-plugin-twenty, twenty-zapier...
|
||||
└───many other packages you are invited to discover such as twenty-docs, twenty-ui, eslint-rules, twenty-zapier...
|
||||
```
|
||||
|
||||
## IDE Setup
|
||||
|
@ -1,16 +1,14 @@
|
||||
module.exports = {
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.stories.tsx', '*.test.ts'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
}
|
||||
},
|
||||
],
|
||||
extends: [
|
||||
'./.eslintrc.cjs'
|
||||
],
|
||||
extends: ['./.eslintrc.cjs'],
|
||||
rules: {
|
||||
'no-console': 'error',
|
||||
}
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['.storybook/**/*', '**/*.stories.tsx', '**/*.test.ts'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -1,56 +1,26 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
extends: [
|
||||
'plugin:@nx/react',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:storybook/recommended',
|
||||
'../../.eslintrc.js',
|
||||
],
|
||||
plugins: ['react-hooks', 'react-refresh'],
|
||||
ignorePatterns: [
|
||||
'!**/*',
|
||||
'node_modules',
|
||||
'mockServiceWorker.js',
|
||||
'**/generated*/*',
|
||||
'*config.*',
|
||||
'**/*config.js',
|
||||
'codegen*',
|
||||
'tsup.ui.index.tsx',
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'simple-import-sort/imports': 'error',
|
||||
'simple-import-sort/exports': 'error',
|
||||
'twenty/effect-components': 'error',
|
||||
'twenty/no-hardcoded-colors': 'error',
|
||||
'twenty/matching-state-variable': 'error',
|
||||
'twenty/component-props-naming': 'error',
|
||||
'twenty/sort-css-properties-alphabetically': 'error',
|
||||
'twenty/styled-components-prefixed-with-styled': 'error',
|
||||
'twenty/no-state-useref': 'error',
|
||||
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
|
||||
'no-unused-vars': 'off',
|
||||
'react/jsx-props-no-spreading': [
|
||||
'error',
|
||||
{
|
||||
explicitSpread: 'ignore',
|
||||
},
|
||||
],
|
||||
'react-hooks/exhaustive-deps': [
|
||||
'warn',
|
||||
{
|
||||
additionalHooks: 'useRecoilCallback',
|
||||
},
|
||||
],
|
||||
'unused-imports/no-unused-imports': 'warn',
|
||||
'unused-imports/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
vars: 'all',
|
||||
varsIgnorePattern: '^_',
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
@ -67,87 +37,58 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{ prefer: 'no-type-imports' },
|
||||
],
|
||||
'no-console': ['warn', { allow: ['group', 'groupCollapsed', 'groupEnd'] }],
|
||||
// 'react-refresh/only-export-components': [
|
||||
// 'warn',
|
||||
// { allowConstantExport: true },
|
||||
// ],
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:storybook/recommended',
|
||||
],
|
||||
plugins: [
|
||||
'@typescript-eslint/eslint-plugin',
|
||||
'simple-import-sort',
|
||||
'unused-imports',
|
||||
'prefer-arrow',
|
||||
'twenty',
|
||||
'react-refresh',
|
||||
],
|
||||
ignorePatterns: [
|
||||
'mockServiceWorker.js',
|
||||
'**/generated*/*',
|
||||
'.eslintrc.cjs',
|
||||
'*.config.cjs',
|
||||
'*.config.ts',
|
||||
'*config.js',
|
||||
'codegen*',
|
||||
'tsup.ui.index.tsx'
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.stories.tsx', '*.test.ts'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
|
||||
rules: {
|
||||
|
||||
'@nx/workspace-effect-components': 'error',
|
||||
'@nx/workspace-no-hardcoded-colors': 'error',
|
||||
'@nx/workspace-matching-state-variable': 'error',
|
||||
'@nx/workspace-sort-css-properties-alphabetically': 'error',
|
||||
'@nx/workspace-styled-components-prefixed-with-styled': 'error',
|
||||
'@nx/workspace-no-state-useref': 'error',
|
||||
'@nx/workspace-component-props-naming': 'error',
|
||||
|
||||
'react/no-unescaped-entities': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react/jsx-key': 'off',
|
||||
'react/display-name': 'off',
|
||||
'react/jsx-uses-react': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'no-control-regex': 0,
|
||||
'no-undef': 'off',
|
||||
'simple-import-sort/imports': [
|
||||
'react/jsx-no-useless-fragment': 'off',
|
||||
'react/jsx-props-no-spreading': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
['^react', '^@?\\w'],
|
||||
['^(@|~)(/.*|$)'],
|
||||
['^\\u0000'],
|
||||
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
|
||||
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
|
||||
['^.+\\.?(css)$'],
|
||||
],
|
||||
explicitSpread: 'ignore',
|
||||
},
|
||||
],
|
||||
'prefer-arrow/prefer-arrow-functions': [
|
||||
'error',
|
||||
|
||||
'react-hooks/exhaustive-deps': [
|
||||
'warn',
|
||||
{
|
||||
disallowPrototype: true,
|
||||
singleReturnOnly: false,
|
||||
classPropertiesAllowed: false,
|
||||
additionalHooks: 'useRecoilCallback',
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
||||
parserOptions: {
|
||||
project: ['packages/twenty-front/tsconfig.*?.json'],
|
||||
},
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: ['.storybook/main.@(js|cjs|mjs|ts)'],
|
||||
rules: {
|
||||
'storybook/no-uninstalled-addons': [
|
||||
'error',
|
||||
{ packageJsonLocation: path.resolve('../../package.json') },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['.storybook/**/*', '**/*.stories.tsx', '**/*.test.@(ts|tsx)'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -1,3 +0,0 @@
|
||||
**/generated*/
|
||||
*.lock
|
||||
*.yaml
|
@ -1,4 +1,4 @@
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
import { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
const computeStoriesGlob = () => {
|
||||
if (process.env.STORYBOOK_SCOPE === 'pages') {
|
||||
@ -6,19 +6,22 @@ const computeStoriesGlob = () => {
|
||||
'../src/pages/**/*.stories.@(js|jsx|ts|tsx)',
|
||||
'../src/__stories__/*.stories.@(js|jsx|ts|tsx)',
|
||||
'../src/pages/**/*.docs.mdx',
|
||||
'../src/__stories__/*.docs.mdx'
|
||||
]
|
||||
'../src/__stories__/*.docs.mdx',
|
||||
];
|
||||
}
|
||||
|
||||
if (process.env.STORYBOOK_SCOPE === 'modules') {
|
||||
return ['../src/modules/**/*.stories.@(js|jsx|ts|tsx)', '../src/modules/**/*.docs.mdx']
|
||||
return [
|
||||
'../src/modules/**/*.stories.@(js|jsx|ts|tsx)',
|
||||
'../src/modules/**/*.docs.mdx',
|
||||
];
|
||||
}
|
||||
|
||||
if (process.env.STORYBOOK_SCOPE === 'ui-docs') {
|
||||
return ['../src/modules/ui/**/*.docs.mdx'];
|
||||
}
|
||||
|
||||
return ['../src/**/*.docs.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)']
|
||||
return ['../src/**/*.docs.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'];
|
||||
};
|
||||
|
||||
const config: StorybookConfig = {
|
||||
|
@ -1,17 +1,18 @@
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
import { withThemeFromJSXProvider } from '@storybook/addon-themes';
|
||||
import { Preview, ReactRenderer } from '@storybook/react';
|
||||
import { withThemeFromJSXProvider } from "@storybook/addon-themes";
|
||||
import { initialize, mswDecorator } from 'msw-storybook-addon';
|
||||
|
||||
import { darkTheme, lightTheme } from '../src/modules/ui/theme/constants/theme';
|
||||
import { RootDecorator } from '../src/testing/decorators/RootDecorator';
|
||||
import { mockedUserJWT } from '../src/testing/mock-data/jwt';
|
||||
|
||||
import { lightTheme, darkTheme } from '../src/modules/ui/theme/constants/theme';
|
||||
import 'react-loading-skeleton/dist/skeleton.css';
|
||||
|
||||
initialize({
|
||||
onUnhandledRequest: async (request: Request) => {
|
||||
const fileExtensionsToIgnore = /\.(ts|tsx|js|jsx|svg|css|png)(\?v=[a-zA-Z0-9]+)?/;
|
||||
const fileExtensionsToIgnore =
|
||||
/\.(ts|tsx|js|jsx|svg|css|png)(\?v=[a-zA-Z0-9]+)?/;
|
||||
|
||||
if (fileExtensionsToIgnore.test(request.url)) {
|
||||
return;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getJestConfig } from "@storybook/test-runner";
|
||||
import { getJestConfig } from '@storybook/test-runner';
|
||||
|
||||
/**
|
||||
* @type {import('@jest/types').Config.InitialOptions}
|
||||
|
@ -11,8 +11,8 @@
|
||||
"tsc": "tsc --watch",
|
||||
"tsc:ci": "tsc",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"lint:ci": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0 --config .eslintrc-ci.cjs",
|
||||
"lint": "eslint . --report-unused-disable-directives --max-warnings 0 --config .eslintrc.cjs",
|
||||
"lint:ci": "yarn lint --config .eslintrc-ci.cjs",
|
||||
"fmt:fix": "prettier --cache --write \"src/**/*.ts\" \"src/**/*.tsx\"",
|
||||
"fmt": "prettier --check \"src/**/*.ts\" \"src/**/*.tsx\"",
|
||||
"test": "jest",
|
||||
@ -49,7 +49,6 @@
|
||||
"@hookform/resolvers": "^3.1.1",
|
||||
"@sentry/react": "^7.88.0",
|
||||
"@sniptt/guards": "^0.2.0",
|
||||
"@swc/core": "^1.3.100",
|
||||
"@swc/jest": "^0.2.29",
|
||||
"@tabler/icons-react": "^2.30.0",
|
||||
"afterframe": "^1.0.2",
|
||||
@ -69,7 +68,6 @@
|
||||
"lodash.kebabcase": "^4.1.1",
|
||||
"lodash.snakecase": "^4.1.1",
|
||||
"luxon": "^3.3.0",
|
||||
"nx": "17.2.3",
|
||||
"react": "^18.2.0",
|
||||
"react-data-grid": "7.0.0-beta.13",
|
||||
"react-datepicker": "^4.11.0",
|
||||
@ -96,72 +94,6 @@
|
||||
"xlsx-ugnis": "^0.19.3",
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^3.3.1",
|
||||
"@graphql-codegen/client-preset": "^4.1.0",
|
||||
"@graphql-codegen/typescript": "^3.0.4",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.4",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@storybook/addon-actions": "^7.6.3",
|
||||
"@storybook/addon-coverage": "^1.0.0",
|
||||
"@storybook/addon-essentials": "^7.6.3",
|
||||
"@storybook/addon-interactions": "^7.6.3",
|
||||
"@storybook/addon-links": "^7.6.3",
|
||||
"@storybook/addon-onboarding": "^1.0.9",
|
||||
"@storybook/addon-themes": "^7.6.3",
|
||||
"@storybook/blocks": "^7.6.3",
|
||||
"@storybook/react": "^7.6.3",
|
||||
"@storybook/react-vite": "^7.6.3",
|
||||
"@storybook/test": "^7.6.3",
|
||||
"@storybook/test-runner": "^0.16.0",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@types/apollo-upload-client": "^17.0.2",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"@types/jest": "^29.5.10",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/lodash.camelcase": "^4.3.7",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/lodash.kebabcase": "^4.1.7",
|
||||
"@types/lodash.snakecase": "^4.1.9",
|
||||
"@types/luxon": "^3.3.0",
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/react": "^18.2.39",
|
||||
"@types/react-datepicker": "^4.11.2",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/scroll-into-view": "^1.16.0",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
"@typescript-eslint/parser": "^6.10.0",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"chromatic": "^6.18.0",
|
||||
"concurrently": "^8.0.1",
|
||||
"cross-var": "^1.1.0",
|
||||
"dotenv-cli": "^7.2.1",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prefer-arrow": "^1.2.3",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.4",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"eslint-plugin-storybook": "^0.6.15",
|
||||
"eslint-plugin-twenty": "file:../eslint-plugin-twenty/eslint-plugin-twenty.tgz",
|
||||
"eslint-plugin-unused-imports": "^3.0.0",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"msw": "^2.0.11",
|
||||
"msw-storybook-addon": "2.0.0--canary.122.b3ed3b1.0",
|
||||
"prettier": "^3.1.0",
|
||||
"storybook": "^7.6.3",
|
||||
"storybook-addon-cookie": "^3.1.0",
|
||||
"storybook-addon-pseudo-states": "^2.1.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.16.0",
|
||||
"npm": "please-use-yarn",
|
||||
|
@ -1,3 +1,3 @@
|
||||
window._env_ = {
|
||||
// This file should stay empty. It will be overwritten by the build process.
|
||||
}
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
export const useFirstMountState = (): boolean => {
|
||||
// eslint-disable-next-line twenty/no-state-useref
|
||||
// eslint-disable-next-line @nx/workspace-no-state-useref
|
||||
const isFirst = useRef(true);
|
||||
|
||||
if (isFirst.current) {
|
||||
|
@ -55,7 +55,7 @@ export const ActivityBodyEditor = ({
|
||||
const imagesActivated = useIsFeatureEnabled('IS_NOTE_CREATE_IMAGES_ENABLED');
|
||||
|
||||
if (!imagesActivated) {
|
||||
slashMenuItems = slashMenuItems.filter((x) => x.name != 'Image');
|
||||
slashMenuItems = slashMenuItems.filter((x) => x.name !== 'Image');
|
||||
}
|
||||
|
||||
const [uploadFile] = useUploadFileMutation();
|
||||
|
@ -102,8 +102,8 @@ export const Attachments = ({
|
||||
fullPath: attachmentUrl,
|
||||
type: getFileType(file.name),
|
||||
companyId:
|
||||
targetableEntity.type == 'Company' ? targetableEntity.id : null,
|
||||
personId: targetableEntity.type == 'Person' ? targetableEntity.id : null,
|
||||
targetableEntity.type === 'Company' ? targetableEntity.id : null,
|
||||
personId: targetableEntity.type === 'Person' ? targetableEntity.id : null,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -42,7 +42,7 @@ export const useHandleCheckableActivityTargetChange = ({
|
||||
.map(([id, _]) => id);
|
||||
|
||||
if (idsToAdd.length) {
|
||||
idsToAdd.map((id) => {
|
||||
idsToAdd.forEach((id) => {
|
||||
const entityFromToSelect = entitiesToSelect.filter(
|
||||
(entity: any) => entity.id === id,
|
||||
).length
|
||||
@ -65,7 +65,7 @@ export const useHandleCheckableActivityTargetChange = ({
|
||||
}
|
||||
|
||||
if (idsToDelete.length) {
|
||||
idsToDelete.map((id) => {
|
||||
idsToDelete.forEach((id) => {
|
||||
const currentActivityTargetId = currentActivityTargets.filter(
|
||||
({ companyId, personId }) => companyId === id || personId === id,
|
||||
)[0].id;
|
||||
|
@ -62,7 +62,7 @@ export const NoteList = ({ title, notes, button }: NoteListProps) => (
|
||||
<NoteCard
|
||||
key={note.id}
|
||||
note={note}
|
||||
isSingleNote={notes.length == 1}
|
||||
isSingleNote={notes.length === 1}
|
||||
/>
|
||||
))}
|
||||
</StyledNoteContainer>
|
||||
|
@ -13,7 +13,7 @@ import { useUpdateEffect } from '~/hooks/useUpdateEffect';
|
||||
import { ApolloFactory } from '../services/apollo.factory';
|
||||
|
||||
export const useApolloFactory = () => {
|
||||
// eslint-disable-next-line twenty/no-state-useref
|
||||
// eslint-disable-next-line @nx/workspace-no-state-useref
|
||||
const apolloRef = useRef<ApolloFactory<NormalizedCacheObject> | null>(null);
|
||||
const [isDebugMode] = useRecoilState(isDebugModeState);
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { OperationType } from '../types/operation-type';
|
||||
|
||||
const operationTypeColors = {
|
||||
// eslint-disable-next-line twenty/no-hardcoded-colors
|
||||
// eslint-disable-next-line @nx/workspace-no-hardcoded-colors
|
||||
query: '#03A9F4',
|
||||
// eslint-disable-next-line twenty/no-hardcoded-colors
|
||||
// eslint-disable-next-line @nx/workspace-no-hardcoded-colors
|
||||
mutation: '#61A600',
|
||||
// eslint-disable-next-line twenty/no-hardcoded-colors
|
||||
// eslint-disable-next-line @nx/workspace-no-hardcoded-colors
|
||||
subscription: '#61A600',
|
||||
// eslint-disable-next-line twenty/no-hardcoded-colors
|
||||
// eslint-disable-next-line @nx/workspace-no-hardcoded-colors
|
||||
error: '#F51818',
|
||||
// eslint-disable-next-line twenty/no-hardcoded-colors
|
||||
// eslint-disable-next-line @nx/workspace-no-hardcoded-colors
|
||||
default: '#61A600',
|
||||
};
|
||||
|
||||
|
@ -68,7 +68,7 @@ export const loggerLink = (getSchemaName: (operation: Operation) => string) =>
|
||||
errors.forEach((err: any) => {
|
||||
logDebug(
|
||||
`%c${err.message}`,
|
||||
// eslint-disable-next-line twenty/no-hardcoded-colors
|
||||
// eslint-disable-next-line @nx/workspace-no-hardcoded-colors
|
||||
'color: #F51818; font-weight: lighter',
|
||||
);
|
||||
});
|
||||
|
@ -157,6 +157,7 @@ export const useAuth = () => {
|
||||
set(supportChatState, supportChat);
|
||||
set(telemetryState, telemetry);
|
||||
set(isDebugModeState, isDebugMode);
|
||||
return undefined;
|
||||
});
|
||||
|
||||
goToRecoilSnapshot(initialSnapshot);
|
||||
|
@ -44,7 +44,7 @@ export const ObjectMetadataNavItems = () => {
|
||||
key={objectMetadataItem.id}
|
||||
label={objectMetadataItem.labelPlural}
|
||||
to={`/objects/${objectMetadataItem.namePlural}`}
|
||||
active={currentPath == `/objects/${objectMetadataItem.namePlural}`}
|
||||
active={currentPath === `/objects/${objectMetadataItem.namePlural}`}
|
||||
Icon={getIcon(objectMetadataItem.icon)}
|
||||
onClick={() => {
|
||||
navigate(`/objects/${objectMetadataItem.namePlural}`);
|
||||
|
@ -36,7 +36,7 @@ const StyledIconTableCell = styled(TableCell)`
|
||||
|
||||
export const SettingsObjectFieldItemTableRow = ({
|
||||
ActionIcon,
|
||||
fieldMetadataItem: fieldMetadataItem,
|
||||
fieldMetadataItem,
|
||||
}: SettingsObjectFieldItemTableRowProps) => {
|
||||
const theme = useTheme();
|
||||
const { getIcon } = useIcons();
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
|
||||
import { Providers } from '@/spreadsheet-import/components/Providers';
|
||||
import { Steps } from '@/spreadsheet-import/steps/components/Steps';
|
||||
import { SpreadsheetOptions } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetOptions as SpreadsheetImportProps } from '@/spreadsheet-import/types';
|
||||
|
||||
export const defaultSpreadsheetImportProps: Partial<SpreadsheetOptions<any>> = {
|
||||
export const defaultSpreadsheetImportProps: Partial<
|
||||
SpreadsheetImportProps<any>
|
||||
> = {
|
||||
autoMapHeaders: true,
|
||||
allowInvalidSubmit: true,
|
||||
autoMapDistance: 2,
|
||||
@ -17,8 +19,7 @@ export const defaultSpreadsheetImportProps: Partial<SpreadsheetOptions<any>> = {
|
||||
} as const;
|
||||
|
||||
export const SpreadsheetImport = <T extends string>(
|
||||
// eslint-disable-next-line twenty/component-props-naming
|
||||
props: SpreadsheetOptions<T>,
|
||||
props: SpreadsheetImportProps<T>,
|
||||
) => {
|
||||
return (
|
||||
<Providers values={props}>
|
||||
|
@ -56,9 +56,9 @@ export const ProgressBar = forwardRef<ProgressBarControls, ProgressBarProps>(
|
||||
const theme = useTheme();
|
||||
|
||||
const controls = useAnimation();
|
||||
// eslint-disable-next-line twenty/no-state-useref
|
||||
// eslint-disable-next-line @nx/workspace-no-state-useref
|
||||
const startTimestamp = useRef<number>(0);
|
||||
// eslint-disable-next-line twenty/no-state-useref
|
||||
// eslint-disable-next-line @nx/workspace-no-state-useref
|
||||
const remainingTime = useRef<number>(duration);
|
||||
|
||||
const start = useCallback(async () => {
|
||||
|
@ -118,7 +118,7 @@ export const SnackBar = ({
|
||||
}: SnackBarProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
// eslint-disable-next-line twenty/no-state-useref
|
||||
// eslint-disable-next-line @nx/workspace-no-state-useref
|
||||
const progressBarRef = useRef<ProgressBarControls | null>(null);
|
||||
|
||||
const closeSnackbar = useCallback(() => {
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
export const usePausableTimeout = (callback: () => void, delay: number) => {
|
||||
// eslint-disable-next-line twenty/no-state-useref
|
||||
// eslint-disable-next-line @nx/workspace-no-state-useref
|
||||
const savedCallback = useRef<() => void>(callback);
|
||||
// eslint-disable-next-line twenty/no-state-useref
|
||||
// eslint-disable-next-line @nx/workspace-no-state-useref
|
||||
const remainingTime = useRef<number>(delay);
|
||||
// eslint-disable-next-line twenty/no-state-useref
|
||||
// eslint-disable-next-line @nx/workspace-no-state-useref
|
||||
const startTime = useRef<number>(Date.now());
|
||||
// eslint-disable-next-line twenty/no-state-useref
|
||||
// eslint-disable-next-line @nx/workspace-no-state-useref
|
||||
const timeoutId = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const tick = () => {
|
||||
|
@ -131,7 +131,7 @@ const TextInputComponent = (
|
||||
tabIndex,
|
||||
RightIcon,
|
||||
}: TextInputComponentProps,
|
||||
// eslint-disable-next-line twenty/component-props-naming
|
||||
// eslint-disable-next-line @nx/workspace-component-props-naming
|
||||
ref: ForwardedRef<HTMLInputElement>,
|
||||
): JSX.Element => {
|
||||
const theme = useTheme();
|
||||
|
@ -23,7 +23,7 @@ const StyledEditor = styled.div`
|
||||
|
||||
export const BlockEditor = ({ editor }: BlockEditorProps) => {
|
||||
const theme = useTheme();
|
||||
const blockNoteTheme = theme.name == 'light' ? 'light' : 'dark';
|
||||
const blockNoteTheme = theme.name === 'light' ? 'light' : 'dark';
|
||||
return (
|
||||
<StyledEditor>
|
||||
<BlockNoteView editor={editor} theme={blockNoteTheme} />
|
||||
|
@ -30,7 +30,7 @@ export const ShowPageMoreButton = ({
|
||||
const navigationMemorizedUrl = useRecoilValue(navigationMemorizedUrlState);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { deleteOneRecord: deleteOneRecord } = useDeleteOneRecord({
|
||||
const { deleteOneRecord } = useDeleteOneRecord({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* eslint-disable twenty/no-hardcoded-colors */
|
||||
/* eslint-disable @nx/workspace-no-hardcoded-colors */
|
||||
import DarkNoise from '../assets/dark-noise.jpg';
|
||||
import LightNoise from '../assets/light-noise.png';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* eslint-disable twenty/no-hardcoded-colors */
|
||||
/* eslint-disable @nx/workspace-no-hardcoded-colors */
|
||||
import hexRgb from 'hex-rgb';
|
||||
|
||||
export const grayScale = {
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* eslint-disable twenty/no-hardcoded-colors */
|
||||
/* eslint-disable @nx/workspace-no-hardcoded-colors */
|
||||
import { accentDark, accentLight } from './accent';
|
||||
import { animation } from './animation';
|
||||
import { backgroundDark, backgroundLight } from './background';
|
||||
|
@ -18,7 +18,7 @@ export const RecoilScope = ({
|
||||
scopeId?: string;
|
||||
CustomRecoilScopeContext?: RecoilScopeContextType;
|
||||
}) => {
|
||||
// eslint-disable-next-line twenty/no-state-useref
|
||||
// eslint-disable-next-line @nx/workspace-no-state-useref
|
||||
const currentScopeId = useRef(scopeId ?? v4());
|
||||
|
||||
return CustomRecoilScopeContext ? (
|
||||
|
@ -28,8 +28,8 @@ export const getViewScopedStatesFromSnapshot = ({
|
||||
availableFieldDefinitionsState,
|
||||
availableFilterDefinitionsState,
|
||||
availableSortDefinitionsState,
|
||||
canPersistFiltersSelector: canPersistFiltersSelector,
|
||||
canPersistSortsSelector: canPersistSortsSelector,
|
||||
canPersistFiltersSelector,
|
||||
canPersistSortsSelector,
|
||||
currentViewFieldsState,
|
||||
currentViewFiltersState,
|
||||
currentViewIdState,
|
||||
@ -41,11 +41,11 @@ export const getViewScopedStatesFromSnapshot = ({
|
||||
onViewFieldsChangeState,
|
||||
onViewFiltersChangeState,
|
||||
onViewSortsChangeState,
|
||||
savedViewFieldsByKeySelector: savedViewFieldsByKeySelector,
|
||||
savedViewFieldsByKeySelector,
|
||||
savedViewFieldsState,
|
||||
savedViewFiltersByKeySelector: savedViewFiltersByKeySelector,
|
||||
savedViewFiltersByKeySelector,
|
||||
savedViewFiltersState,
|
||||
savedViewSortsByKeySelector: savedViewSortsByKeySelector,
|
||||
savedViewSortsByKeySelector,
|
||||
savedViewSortsState,
|
||||
viewEditModeState,
|
||||
viewObjectMetadataIdState,
|
||||
|
@ -87,7 +87,7 @@ export const SettingsObjectNewFieldStep1 = () => {
|
||||
metadataField.isActive ===
|
||||
activeObjectMetadataItem.fields[index].isActive
|
||||
) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return metadataField.isActive
|
||||
|
14
packages/twenty-front/tsconfig.app.json
Normal file
14
packages/twenty-front/tsconfig.app.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc"
|
||||
},
|
||||
"exclude": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.tsx",
|
||||
"jest.config.ts"
|
||||
],
|
||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
||||
}
|
@ -29,6 +29,17 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
16
packages/twenty-front/tsconfig.spec.json
Normal file
16
packages/twenty-front/tsconfig.spec.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.ts",
|
||||
"**/*.d.ts",
|
||||
".storybook/**/*",
|
||||
"**/*.stories.tsx"
|
||||
]
|
||||
}
|
@ -111,13 +111,12 @@
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
"@nestjs/schematics": "^9.0.0",
|
||||
"@nestjs/testing": "^9.0.0",
|
||||
"@stylistic/eslint-plugin": "^1.5.0",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/bytes": "^3.1.1",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/graphql-fields": "^1.3.6",
|
||||
"@types/graphql-upload": "^8.0.12",
|
||||
"@types/jest": "28.1.8",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash.isempty": "^4.4.7",
|
||||
"@types/lodash.isequal": "^4.5.7",
|
||||
"@types/lodash.isobject": "^3.0.7",
|
||||
@ -125,27 +124,15 @@
|
||||
"@types/lodash.snakecase": "^4.1.7",
|
||||
"@types/lodash.upperfirst": "^4.3.7",
|
||||
"@types/ms": "^0.7.31",
|
||||
"@types/node": "^16.0.0",
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/passport-google-oauth20": "^2.0.11",
|
||||
"@types/passport-jwt": "^3.0.8",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-unused-imports": "^3.0.0",
|
||||
"jest": "28.1.3",
|
||||
"prettier": "^2.3.2",
|
||||
"source-map-support": "^0.5.20",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-jest": "28.0.8",
|
||||
"ts-loader": "^9.2.3",
|
||||
"ts-node": "^10.0.0",
|
||||
"tsconfig-paths": "4.1.0",
|
||||
"typescript": "^4.9.4"
|
||||
"tsconfig-paths": "4.1.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"graphql": "16.8.0"
|
||||
|
@ -30,9 +30,8 @@ export class GoogleGmailService {
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const workspaceDataSource = await this.typeORMService.connectToDataSource(
|
||||
dataSourceMetadata,
|
||||
);
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
const connectedAccount = await workspaceDataSource?.query(
|
||||
`SELECT * FROM ${dataSourceMetadata.schema}."connectedAccount" WHERE "handle" = $1 AND "provider" = $2 AND "accountOwnerId" = $3`,
|
||||
|
@ -51,9 +51,8 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
const workspaceDataSource = await this.typeORMService.connectToDataSource(
|
||||
dataSourceMetadata,
|
||||
);
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
const apiKey = await workspaceDataSource?.query(
|
||||
`SELECT * FROM ${dataSourceMetadata.schema}."apiKey" WHERE id = '${payload.jti}'`,
|
||||
|
@ -16,9 +16,8 @@ export class TimelineMessagingService {
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const workspaceDataSource = await this.typeORMService.connectToDataSource(
|
||||
dataSourceMetadata,
|
||||
);
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
// 10 first threads This hard limit is just for the POC, we will implement pagination later
|
||||
const messageThreads = await workspaceDataSource?.query(
|
||||
@ -80,9 +79,8 @@ export class TimelineMessagingService {
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const workspaceDataSource = await this.typeORMService.connectToDataSource(
|
||||
dataSourceMetadata,
|
||||
);
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
const personIds = await workspaceDataSource?.query(
|
||||
`
|
||||
|
@ -106,11 +106,14 @@ const computeSchemaComponent = (
|
||||
|
||||
if (requiredFields?.length) {
|
||||
result.required = requiredFields;
|
||||
result.example = requiredFields.reduce((example, requiredField) => {
|
||||
result.example = requiredFields.reduce(
|
||||
(example, requiredField) => {
|
||||
example[requiredField] = '';
|
||||
|
||||
return example;
|
||||
}, {} as Record<string, string>);
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -119,11 +122,14 @@ const computeSchemaComponent = (
|
||||
export const computeSchemaComponents = (
|
||||
objectMetadataItems: ObjectMetadataEntity[],
|
||||
): Record<string, OpenAPIV3.SchemaObject> => {
|
||||
return objectMetadataItems.reduce((schemas, item) => {
|
||||
return objectMetadataItems.reduce(
|
||||
(schemas, item) => {
|
||||
schemas[capitalize(item.nameSingular)] = computeSchemaComponent(item);
|
||||
|
||||
return schemas;
|
||||
}, {} as Record<string, OpenAPIV3.SchemaObject>);
|
||||
},
|
||||
{} as Record<string, OpenAPIV3.SchemaObject>,
|
||||
);
|
||||
};
|
||||
|
||||
export const computeParameterComponents = (): Record<
|
||||
|
@ -25,9 +25,8 @@ export class UserService extends TypeOrmQueryService<User> {
|
||||
user.defaultWorkspace.id,
|
||||
);
|
||||
|
||||
const workspaceDataSource = await this.typeORMService.connectToDataSource(
|
||||
dataSourceMetadata,
|
||||
);
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
const workspaceMembers = await workspaceDataSource?.query(
|
||||
`SELECT * FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${user.id}'`,
|
||||
@ -55,9 +54,8 @@ export class UserService extends TypeOrmQueryService<User> {
|
||||
user.defaultWorkspace.id,
|
||||
);
|
||||
|
||||
const workspaceDataSource = await this.typeORMService.connectToDataSource(
|
||||
dataSourceMetadata,
|
||||
);
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
await workspaceDataSource?.query(
|
||||
`INSERT INTO ${dataSourceMetadata.schema}."workspaceMember"
|
||||
|
@ -76,9 +76,8 @@ export class DataSeedWorkspaceCommand extends CommandRunner {
|
||||
this.workspaceId,
|
||||
);
|
||||
|
||||
const workspaceDataSource = await this.typeORMService.connectToDataSource(
|
||||
dataSourceMetadata,
|
||||
);
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
if (!workspaceDataSource) {
|
||||
throw new Error('Could not connect to workspace data source');
|
||||
|
@ -49,9 +49,8 @@ export class TypeORMService implements OnModuleInit, OnModuleDestroy {
|
||||
this.isDatasourceInitializing.set(dataSource.id, true);
|
||||
|
||||
try {
|
||||
const dataSourceInstance = await this.createAndInitializeDataSource(
|
||||
dataSource,
|
||||
);
|
||||
const dataSourceInstance =
|
||||
await this.createAndInitializeDataSource(dataSource);
|
||||
|
||||
this.dataSources.set(dataSource.id, dataSourceInstance);
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
|
||||
import { QueueJobOptions } from 'src/integrations/message-queue/drivers/interfaces/job-options.interface';
|
||||
import { MessageQueueDriver } from 'src/integrations/message-queue/drivers/interfaces/message-queue-driver.interface';
|
||||
import {
|
||||
MessageQueueJob,
|
||||
@ -17,7 +16,6 @@ export class SyncDriver implements MessageQueueDriver {
|
||||
_queueName: MessageQueue,
|
||||
jobName: string,
|
||||
data: T,
|
||||
_options?: QueueJobOptions | undefined,
|
||||
): Promise<void> {
|
||||
const jobClassName = getJobClassName(jobName);
|
||||
const job: MessageQueueJob<MessageQueueJobData> = this.jobsModuleRef.get(
|
||||
@ -28,10 +26,7 @@ export class SyncDriver implements MessageQueueDriver {
|
||||
return await job.handle(data);
|
||||
}
|
||||
|
||||
work<T>(
|
||||
queueName: MessageQueue,
|
||||
handler: ({ data, id }: { data: T; id: string }) => void | Promise<void>,
|
||||
) {
|
||||
work() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -112,9 +112,8 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
fieldMetadataInput.workspaceId,
|
||||
);
|
||||
|
||||
const workspaceDataSource = await this.typeORMService.connectToDataSource(
|
||||
dataSourceMetadata,
|
||||
);
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
// TODO: use typeorm repository
|
||||
const view = await workspaceDataSource?.query(
|
||||
|
@ -281,9 +281,8 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
createdObjectMetadata.workspaceId,
|
||||
);
|
||||
|
||||
const workspaceDataSource = await this.typeORMService.connectToDataSource(
|
||||
dataSourceMetadata,
|
||||
);
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
const view = await workspaceDataSource?.query(
|
||||
`INSERT INTO ${dataSourceMetadata.schema}."view"
|
||||
|
@ -100,11 +100,14 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
|
||||
},
|
||||
);
|
||||
|
||||
const objectMetadataMap = objectMetadataEntries.reduce((acc, curr) => {
|
||||
const objectMetadataMap = objectMetadataEntries.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr.id] = curr;
|
||||
|
||||
return acc;
|
||||
}, {} as { [key: string]: ObjectMetadataEntity });
|
||||
},
|
||||
{} as { [key: string]: ObjectMetadataEntity },
|
||||
);
|
||||
|
||||
if (
|
||||
objectMetadataMap[relationMetadataInput.fromObjectMetadataId] ===
|
||||
|
@ -25,7 +25,7 @@ export function mergeDefaultOptions<
|
||||
return query.getRawMany();
|
||||
},
|
||||
getCursor: (record: Record | undefined) =>
|
||||
({ id: (record as unknown as { id: string })?.id } as unknown as Cursor),
|
||||
({ id: (record as unknown as { id: string })?.id }) as unknown as Cursor,
|
||||
encodeCursor: (cursor: Cursor) =>
|
||||
Buffer.from((cursor as unknown as { id: string }).id.toString()).toString(
|
||||
'base64',
|
||||
@ -33,9 +33,9 @@ export function mergeDefaultOptions<
|
||||
decodeCursor: (cursorString: string) =>
|
||||
({
|
||||
id: Buffer.from(cursorString, 'base64').toString(),
|
||||
} as unknown as Cursor),
|
||||
}) as unknown as Cursor,
|
||||
recordToEdge: (record: Record) =>
|
||||
({ node: record } as unknown as Omit<CustomEdge, 'cursor'>),
|
||||
({ node: record }) as unknown as Omit<CustomEdge, 'cursor'>,
|
||||
resolveInfo: null,
|
||||
...pOptions,
|
||||
};
|
||||
|
@ -29,9 +29,8 @@ export class FetchBatchMessagesService {
|
||||
'batch_gmail_messages',
|
||||
);
|
||||
|
||||
const messages = await this.formatBatchResponsesAsGmailMessages(
|
||||
batchResponses,
|
||||
);
|
||||
const messages =
|
||||
await this.formatBatchResponsesAsGmailMessages(batchResponses);
|
||||
|
||||
return messages;
|
||||
}
|
||||
@ -46,9 +45,8 @@ export class FetchBatchMessagesService {
|
||||
'batch_gmail_threads',
|
||||
);
|
||||
|
||||
const threads = await this.formatBatchResponsesAsGmailThreads(
|
||||
batchResponses,
|
||||
);
|
||||
const threads =
|
||||
await this.formatBatchResponsesAsGmailThreads(batchResponses);
|
||||
|
||||
return threads;
|
||||
}
|
||||
@ -242,9 +240,8 @@ export class FetchBatchMessagesService {
|
||||
): Promise<GmailMessage[]> {
|
||||
const formattedResponses = await Promise.all(
|
||||
batchResponses.map(async (response) => {
|
||||
const formattedResponse = await this.formatBatchResponseAsGmailMessage(
|
||||
response,
|
||||
);
|
||||
const formattedResponse =
|
||||
await this.formatBatchResponseAsGmailMessage(response);
|
||||
|
||||
return formattedResponse;
|
||||
}),
|
||||
@ -292,9 +289,8 @@ export class FetchBatchMessagesService {
|
||||
): Promise<GmailThread[]> {
|
||||
const formattedResponses = await Promise.all(
|
||||
batchResponses.map(async (response) => {
|
||||
const formattedResponse = await this.formatBatchResponseAsGmailThread(
|
||||
response,
|
||||
);
|
||||
const formattedResponse =
|
||||
await this.formatBatchResponseAsGmailThread(response);
|
||||
|
||||
return formattedResponse;
|
||||
}),
|
||||
|
@ -44,9 +44,8 @@ export class FetchWorkspaceMessagesService {
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const workspaceDataSource = await this.typeORMService.connectToDataSource(
|
||||
dataSourceMetadata,
|
||||
);
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
if (!workspaceDataSource) {
|
||||
throw new Error('No workspace data source found');
|
||||
|
@ -23,9 +23,8 @@ export class RefreshAccessTokenService {
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const workspaceDataSource = await this.typeORMService.connectToDataSource(
|
||||
dataSourceMetadata,
|
||||
);
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
if (!workspaceDataSource) {
|
||||
throw new Error('No workspace data source found');
|
||||
|
@ -27,9 +27,8 @@ export class WorkspaceDataSourceService {
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const dataSource = await this.typeormService.connectToDataSource(
|
||||
dataSourceMetadata,
|
||||
);
|
||||
const dataSource =
|
||||
await this.typeormService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
if (!dataSource) {
|
||||
throw new Error(
|
||||
|
@ -72,14 +72,17 @@ export class EnumTypeDefinitionFactory {
|
||||
return new GraphQLEnumType({
|
||||
name: `${pascalCase(objectName)}${pascalCase(fieldMetadata.name)}Enum`,
|
||||
description: fieldMetadata.description,
|
||||
values: enumOptions.reduce((acc, enumOption) => {
|
||||
values: enumOptions.reduce(
|
||||
(acc, enumOption) => {
|
||||
acc[enumOption.value] = {
|
||||
value: enumOption.value,
|
||||
description: enumOption.label,
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {} as { [key: string]: { value: string; description: string } }),
|
||||
},
|
||||
{} as { [key: string]: { value: string; description: string } },
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -28,9 +28,8 @@ export class WorkspaceSchemaStorageService {
|
||||
(await this.cacheVersionMemoryStorageService.read({
|
||||
key: workspaceId,
|
||||
})) ?? '0';
|
||||
const latestVersion = await this.workspaceCacheVersionService.getVersion(
|
||||
workspaceId,
|
||||
);
|
||||
const latestVersion =
|
||||
await this.workspaceCacheVersionService.getVersion(workspaceId);
|
||||
|
||||
if (currentVersion !== latestVersion) {
|
||||
// Invalidate cache if version mismatch is detected
|
||||
|
@ -36,18 +36,24 @@ export const mapObjectMetadataByUniqueIdentifier = <
|
||||
>(
|
||||
arr: T[],
|
||||
): Record<string, Omit<T, 'fields'> & { fields: Record<string, U> }> => {
|
||||
return arr.reduce((acc, curr) => {
|
||||
return arr.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr.nameSingular] = {
|
||||
...curr,
|
||||
fields: curr.fields.reduce((acc, curr) => {
|
||||
fields: curr.fields.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr.name] = curr;
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, U>),
|
||||
},
|
||||
{} as Record<string, U>,
|
||||
),
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, Omit<T, 'fields'> & { fields: Record<string, U> }>);
|
||||
},
|
||||
{} as Record<string, Omit<T, 'fields'> & { fields: Record<string, U> }>,
|
||||
);
|
||||
};
|
||||
|
||||
export const convertStringifiedFieldsToJSON = <
|
||||
|
@ -405,7 +405,7 @@ export class WorkspaceSyncMetadataService {
|
||||
WorkspaceMigrationColumnActionType.CREATE,
|
||||
field,
|
||||
),
|
||||
} satisfies WorkspaceMigrationTableAction),
|
||||
}) satisfies WorkspaceMigrationTableAction,
|
||||
),
|
||||
];
|
||||
|
||||
@ -420,11 +420,14 @@ export class WorkspaceSyncMetadataService {
|
||||
// TODO: handle object delete migrations.
|
||||
// Note: we need to delete the relation first due to the DB constraint.
|
||||
|
||||
const objectsInDbById = objectsInDB.reduce((result, currentObject) => {
|
||||
const objectsInDbById = objectsInDB.reduce(
|
||||
(result, currentObject) => {
|
||||
result[currentObject.id] = currentObject;
|
||||
|
||||
return result;
|
||||
}, {} as Record<string, ObjectMetadataEntity>);
|
||||
},
|
||||
{} as Record<string, ObjectMetadataEntity>,
|
||||
);
|
||||
|
||||
if (fieldsToCreate.length > 0) {
|
||||
fieldsToCreate.map((field) => {
|
||||
|
@ -60,9 +60,8 @@ export class WorkspaceFactory {
|
||||
}
|
||||
|
||||
// Get typeDefs from cache
|
||||
let typeDefs = await this.workspaceSchemaStorageService.getTypeDefs(
|
||||
workspaceId,
|
||||
);
|
||||
let typeDefs =
|
||||
await this.workspaceSchemaStorageService.getTypeDefs(workspaceId);
|
||||
let usedScalarNames =
|
||||
await this.workspaceSchemaStorageService.getUsedScalarNames(workspaceId);
|
||||
|
||||
|
@ -22,6 +22,6 @@
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"resolveJsonModule": true,
|
||||
"typeRoots": ["@types", "node_modules/@types"]
|
||||
"types": ["jest", "node"]
|
||||
}
|
||||
}
|
||||
|
64
tools/eslint-rules/index.ts
Normal file
64
tools/eslint-rules/index.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import {
|
||||
rule as componentPropsNaming,
|
||||
RULE_NAME as componentPropsNamingName,
|
||||
} from './rules/component-props-naming';
|
||||
import {
|
||||
rule as effectComponents,
|
||||
RULE_NAME as effectComponentsName,
|
||||
} from './rules/effect-components';
|
||||
import {
|
||||
rule as matchingStateVariable,
|
||||
RULE_NAME as matchingStateVariableName,
|
||||
} from './rules/matching-state-variable';
|
||||
import {
|
||||
rule as noHardcodedColors,
|
||||
RULE_NAME as noHardcodedColorsName,
|
||||
} from './rules/no-hardcoded-colors';
|
||||
import {
|
||||
rule as noStateUseref,
|
||||
RULE_NAME as noStateUserefName,
|
||||
} from './rules/no-state-useref';
|
||||
import {
|
||||
rule as sortCssPropertiesAlphabetically,
|
||||
RULE_NAME as sortCssPropertiesAlphabeticallyName,
|
||||
} from './rules/sort-css-properties-alphabetically';
|
||||
import {
|
||||
rule as styledComponentsPrefixedWithStyled,
|
||||
RULE_NAME as styledComponentsPrefixedWithStyledName,
|
||||
} from './rules/styled-components-prefixed-with-styled';
|
||||
/**
|
||||
* Import your custom workspace rules at the top of this file.
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* import { RULE_NAME as myCustomRuleName, rule as myCustomRule } from './rules/my-custom-rule';
|
||||
*
|
||||
* In order to quickly get started with writing rules you can use the
|
||||
* following generator command and provide your desired rule name:
|
||||
*
|
||||
* ```sh
|
||||
* npx nx g @nx/eslint:workspace-rule {{ NEW_RULE_NAME }}
|
||||
* ```
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Apply the imported custom rules here.
|
||||
*
|
||||
* For example (using the example import above):
|
||||
*
|
||||
* rules: {
|
||||
* [myCustomRuleName]: myCustomRule
|
||||
* }
|
||||
*/
|
||||
rules: {
|
||||
[componentPropsNamingName]: componentPropsNaming,
|
||||
[effectComponentsName]: effectComponents,
|
||||
[matchingStateVariableName]: matchingStateVariable,
|
||||
[noHardcodedColorsName]: noHardcodedColors,
|
||||
[noStateUserefName]: noStateUseref,
|
||||
[sortCssPropertiesAlphabeticallyName]: sortCssPropertiesAlphabetically,
|
||||
[styledComponentsPrefixedWithStyledName]:
|
||||
styledComponentsPrefixedWithStyled,
|
||||
},
|
||||
};
|
10
tools/eslint-rules/jest.config.ts
Normal file
10
tools/eslint-rules/jest.config.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'eslint-rules',
|
||||
preset: '../../jest.preset.js',
|
||||
transform: {
|
||||
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: '../../coverage/tools/eslint-rules',
|
||||
};
|
28
tools/eslint-rules/project.json
Normal file
28
tools/eslint-rules/project.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "eslint-rules",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "tools/eslint-rules",
|
||||
"targets": {
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"outputs": [
|
||||
"{options.outputFile}"
|
||||
],
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"tools/eslint-rules/**/*.ts"
|
||||
],
|
||||
"fix": true
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": [
|
||||
"{workspaceRoot}/coverage/{projectRoot}"
|
||||
],
|
||||
"options": {
|
||||
"jestConfig": "tools/eslint-rules/jest.config.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
45
tools/eslint-rules/rules/component-props-naming.spec.ts
Normal file
45
tools/eslint-rules/rules/component-props-naming.spec.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { TSESLint } from '@typescript-eslint/utils';
|
||||
|
||||
import { rule, RULE_NAME } from './component-props-naming';
|
||||
|
||||
const ruleTester = new TSESLint.RuleTester({
|
||||
parser: require.resolve('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run(RULE_NAME, rule, {
|
||||
valid: [
|
||||
{
|
||||
code: 'export const MyComponent= (props: MyComponentProps) => <div>{props.message}</div>;',
|
||||
},
|
||||
{
|
||||
code: 'export const MyComponent = ({ message }: MyComponentProps) => <div>{message}</div>;',
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: 'export const MyComponent = (props: OwnProps) => <div>{props.message}</div>;',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'invalidPropsTypeName',
|
||||
},
|
||||
],
|
||||
output:
|
||||
'export const MyComponent = (props: MyComponentProps) => <div>{props.message}</div>;',
|
||||
},
|
||||
{
|
||||
code: 'export const MyComponent = ({ message }: OwnProps) => <div>{message}</div>;',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'invalidPropsTypeName',
|
||||
},
|
||||
],
|
||||
output:
|
||||
'export const MyComponent = ({ message }: MyComponentProps) => <div>{message}</div>;',
|
||||
},
|
||||
],
|
||||
});
|
78
tools/eslint-rules/rules/component-props-naming.ts
Normal file
78
tools/eslint-rules/rules/component-props-naming.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
|
||||
import {
|
||||
isIdentifier,
|
||||
isVariableDeclarator,
|
||||
} from '@typescript-eslint/utils/ast-utils';
|
||||
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
|
||||
|
||||
// NOTE: The rule will be available in ESLint configs as "@nx/workspace-component-props-naming"
|
||||
export const RULE_NAME = 'component-props-naming';
|
||||
|
||||
const checkPropsTypeName = ({
|
||||
node,
|
||||
context,
|
||||
functionName,
|
||||
}: {
|
||||
node: TSESTree.FunctionDeclaration | TSESTree.ArrowFunctionExpression;
|
||||
context: Readonly<RuleContext<'invalidPropsTypeName', any[]>>;
|
||||
functionName: string;
|
||||
}) => {
|
||||
const expectedPropTypeName = `${functionName}Props`;
|
||||
|
||||
if (!functionName.match(/^[A-Z]/)) return;
|
||||
|
||||
node.params.forEach((param) => {
|
||||
if (
|
||||
(param.type === TSESTree.AST_NODE_TYPES.ObjectPattern ||
|
||||
isIdentifier(param)) &&
|
||||
param.typeAnnotation?.typeAnnotation.type ===
|
||||
TSESTree.AST_NODE_TYPES.TSTypeReference &&
|
||||
isIdentifier(param.typeAnnotation?.typeAnnotation.typeName)
|
||||
) {
|
||||
const { typeName } = param.typeAnnotation.typeAnnotation;
|
||||
const actualPropTypeName = typeName.name;
|
||||
if (actualPropTypeName !== expectedPropTypeName) {
|
||||
context.report({
|
||||
node: param,
|
||||
messageId: 'invalidPropsTypeName',
|
||||
data: { expectedPropTypeName, actualPropTypeName },
|
||||
fix: (fixer) => fixer.replaceText(typeName, expectedPropTypeName),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const rule = ESLintUtils.RuleCreator(() => __filename)({
|
||||
name: RULE_NAME,
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Ensure component props follow naming convention',
|
||||
recommended: 'recommended',
|
||||
},
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
messages: {
|
||||
invalidPropsTypeName:
|
||||
"Expected prop type to be '{{ expectedPropTypeName }}' but found '{{ actualPropTypeName }}'",
|
||||
},
|
||||
},
|
||||
defaultOptions: [],
|
||||
create: (context) => {
|
||||
return {
|
||||
ArrowFunctionExpression: (node) => {
|
||||
if (isVariableDeclarator(node.parent) && isIdentifier(node.parent.id)) {
|
||||
checkPropsTypeName({
|
||||
node,
|
||||
context,
|
||||
functionName: node.parent.id.name,
|
||||
});
|
||||
}
|
||||
},
|
||||
FunctionDeclaration: (node) => {
|
||||
checkPropsTypeName({ node, context, functionName: node.id.name });
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
@ -1,19 +1,17 @@
|
||||
import { RuleTester } from "@typescript-eslint/rule-tester";
|
||||
import { TSESLint } from '@typescript-eslint/utils';
|
||||
|
||||
import effectComponentsRule from "../rules/effect-components";
|
||||
import { rule, RULE_NAME } from '../rules/effect-components';
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: "@typescript-eslint/parser",
|
||||
const ruleTester = new TSESLint.RuleTester({
|
||||
parser: require.resolve('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run("effect-components", effectComponentsRule, {
|
||||
ruleTester.run(RULE_NAME, rule, {
|
||||
valid: [
|
||||
{
|
||||
code: `const TestComponentEffect = () => <></>;`,
|
||||
@ -64,20 +62,20 @@ ruleTester.run("effect-components", effectComponentsRule, {
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "const TestComponent = () => <></>;",
|
||||
output: "const TestComponentEffect = () => <></>;",
|
||||
code: 'const TestComponent = () => <></>;',
|
||||
output: 'const TestComponentEffect = () => <></>;',
|
||||
errors: [
|
||||
{
|
||||
messageId: "effectSuffix",
|
||||
messageId: 'addEffectSuffix',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "const TestComponentEffect = () => <><div></div></>;",
|
||||
output: "const TestComponent = () => <><div></div></>;",
|
||||
code: 'const TestComponentEffect = () => <><div></div></>;',
|
||||
output: 'const TestComponent = () => <><div></div></>;',
|
||||
errors: [
|
||||
{
|
||||
messageId: "noEffectSuffix",
|
||||
messageId: 'removeEffectSuffix',
|
||||
},
|
||||
],
|
||||
},
|
115
tools/eslint-rules/rules/effect-components.ts
Normal file
115
tools/eslint-rules/rules/effect-components.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
|
||||
import {
|
||||
isIdentifier,
|
||||
isVariableDeclarator,
|
||||
} from '@typescript-eslint/utils/ast-utils';
|
||||
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
|
||||
|
||||
// NOTE: The rule will be available in ESLint configs as "@nx/workspace-effect-components"
|
||||
export const RULE_NAME = 'effect-components';
|
||||
|
||||
const isPascalCase = (input: string) => !!input.match(/^[A-Z][a-zA-Z0-9_]*/);
|
||||
|
||||
type TargetNode =
|
||||
| TSESTree.ArrowFunctionExpression
|
||||
| TSESTree.FunctionDeclaration
|
||||
| TSESTree.FunctionExpression;
|
||||
|
||||
const isReturningEmptyFragmentOrNull = (node: TargetNode) =>
|
||||
// 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 checkEffectComponent = ({
|
||||
context,
|
||||
identifier,
|
||||
node,
|
||||
}: {
|
||||
context: Readonly<
|
||||
RuleContext<'addEffectSuffix' | 'removeEffectSuffix', any[]>
|
||||
>;
|
||||
identifier: TSESTree.Identifier;
|
||||
node: TargetNode;
|
||||
}) => {
|
||||
const componentName = identifier.name;
|
||||
|
||||
if (!isPascalCase(componentName)) return;
|
||||
|
||||
const isEffectComponent = isReturningEmptyFragmentOrNull(node);
|
||||
const hasEffectSuffix = componentName.endsWith('Effect');
|
||||
|
||||
if (isEffectComponent && !hasEffectSuffix) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'addEffectSuffix',
|
||||
data: { componentName },
|
||||
fix: (fixer) => fixer.replaceText(identifier, componentName + 'Effect'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasEffectSuffix && !isEffectComponent) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'removeEffectSuffix',
|
||||
data: { componentName },
|
||||
fix: (fixer) =>
|
||||
fixer.replaceText(identifier, componentName.replace('Effect', '')),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const rule = ESLintUtils.RuleCreator(() => __filename)({
|
||||
name: RULE_NAME,
|
||||
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: {
|
||||
addEffectSuffix:
|
||||
'Effect component {{ componentName }} should end with the Effect suffix.',
|
||||
removeEffectSuffix:
|
||||
"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: [],
|
||||
create: (context) => {
|
||||
const checkFunctionExpressionEffectComponent = (
|
||||
node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
|
||||
) =>
|
||||
isVariableDeclarator(node.parent) && isIdentifier(node.parent.id)
|
||||
? checkEffectComponent({ context, identifier: node.parent.id, node })
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
ArrowFunctionExpression: checkFunctionExpressionEffectComponent,
|
||||
|
||||
FunctionDeclaration: (node) =>
|
||||
checkEffectComponent({ context, identifier: node.id, node }),
|
||||
|
||||
FunctionExpression: checkFunctionExpressionEffectComponent,
|
||||
};
|
||||
},
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user