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:
Thaïs 2024-01-03 19:07:25 -03:00 committed by GitHub
parent 1924962e8c
commit 8483cf0b4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
125 changed files with 2547 additions and 3161 deletions

99
.eslintrc.js Normal file
View 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: {},
},
],
};

5
.gitignore vendored
View File

@ -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
**/yarn.lock

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
# Add files here to ignore them from prettier formatting
/dist
/coverage
/.nx/cache

View File

@ -2,4 +2,4 @@
"singleQuote": true,
"trailingComma": "all",
"endOfLine": "auto"
}
}

View File

@ -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"
]
}

View File

@ -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
View File

@ -0,0 +1,5 @@
import { getJestProjects } from '@nx/jest';
export default {
projects: getJestProjects(),
};

3
jest.preset.js Normal file
View File

@ -0,0 +1,3 @@
const nxPreset = require('@nx/jest/preset').default;
module.exports = { ...nxPreset };

24
nx.json
View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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" }],
}
};

View File

@ -1 +0,0 @@
dist/

View File

@ -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"),
},
};

View File

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

View File

@ -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"
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>;",
},
],
});

View File

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

View File

@ -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);",
},
],
});

View File

@ -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",
},
],
},
],
});

View File

@ -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",
},
],
},
],
});

View File

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

View File

@ -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",
},
],
},
],
},
);

View File

@ -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",
},
],
},
],
},
);

View File

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

View File

@ -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",
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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',
},
},
],
};

View File

@ -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': [
'@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',
'react/jsx-no-useless-fragment': 'off',
'react/jsx-props-no-spreading': [
'error',
{ prefer: 'no-type-imports' },
{
explicitSpread: 'ignore',
},
],
'react-hooks/exhaustive-deps': [
'warn',
{
additionalHooks: 'useRecoilCallback',
},
],
'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'],
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',
},
},
{
files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
rules: {
'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': [
'error',
{
groups: [
['^react', '^@?\\w'],
['^(@|~)(/.*|$)'],
['^\\u0000'],
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
['^.+\\.?(css)$'],
],
},
],
'prefer-arrow/prefer-arrow-functions': [
'error',
{
disallowPrototype: true,
singleReturnOnly: false,
classPropertiesAllowed: false,
},
],
},
},
],
};

View File

@ -1,3 +0,0 @@
**/generated*/
*.lock
*.yaml

View File

@ -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/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 = {

View File

@ -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;

View File

@ -1,4 +1,4 @@
import { getJestConfig } from "@storybook/test-runner";
import { getJestConfig } from '@storybook/test-runner';
/**
* @type {import('@jest/types').Config.InitialOptions}
@ -10,4 +10,4 @@ export default {
* @see https://jestjs.io/docs/configuration
*/
testTimeout: process.env.STORYBOOK_SCOPE === 'pages' ? 60000 : 15000,
};
};

View File

@ -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",

View File

@ -1,3 +1,3 @@
window._env_ = {
// This file should stay empty. It will be overwritten by the build process.
}
};

View File

@ -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) {

View File

@ -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();

View File

@ -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,
});
};

View File

@ -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;

View File

@ -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>

View File

@ -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);

View File

@ -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',
};

View File

@ -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',
);
});

View File

@ -157,6 +157,7 @@ export const useAuth = () => {
set(supportChatState, supportChat);
set(telemetryState, telemetry);
set(isDebugModeState, isDebugMode);
return undefined;
});
goToRecoilSnapshot(initialSnapshot);

View File

@ -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}`);

View File

@ -36,7 +36,7 @@ const StyledIconTableCell = styled(TableCell)`
export const SettingsObjectFieldItemTableRow = ({
ActionIcon,
fieldMetadataItem: fieldMetadataItem,
fieldMetadataItem,
}: SettingsObjectFieldItemTableRowProps) => {
const theme = useTheme();
const { getIcon } = useIcons();

View File

@ -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}>

View File

@ -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 () => {

View File

@ -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(() => {

View File

@ -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 = () => {

View File

@ -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();

View File

@ -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} />

View File

@ -30,7 +30,7 @@ export const ShowPageMoreButton = ({
const navigationMemorizedUrl = useRecoilValue(navigationMemorizedUrlState);
const navigate = useNavigate();
const { deleteOneRecord: deleteOneRecord } = useDeleteOneRecord({
const { deleteOneRecord } = useDeleteOneRecord({
objectNameSingular,
});

View File

@ -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';

View File

@ -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 = {

View File

@ -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';

View File

@ -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 ? (

View File

@ -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,

View File

@ -87,7 +87,7 @@ export const SettingsObjectNewFieldStep1 = () => {
metadataField.isActive ===
activeObjectMetadataItem.fields[index].isActive
) {
return;
return undefined;
}
return metadataField.isActive

View 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"]
}

View File

@ -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"
}
]
}

View 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"
]
}

View File

@ -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"

View File

@ -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`,

View File

@ -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}'`,

View File

@ -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(
`

View File

@ -106,11 +106,14 @@ const computeSchemaComponent = (
if (requiredFields?.length) {
result.required = requiredFields;
result.example = requiredFields.reduce((example, requiredField) => {
example[requiredField] = '';
result.example = requiredFields.reduce(
(example, requiredField) => {
example[requiredField] = '';
return example;
}, {} as Record<string, string>);
return example;
},
{} 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) => {
schemas[capitalize(item.nameSingular)] = computeSchemaComponent(item);
return objectMetadataItems.reduce(
(schemas, item) => {
schemas[capitalize(item.nameSingular)] = computeSchemaComponent(item);
return schemas;
}, {} as Record<string, OpenAPIV3.SchemaObject>);
return schemas;
},
{} as Record<string, OpenAPIV3.SchemaObject>,
);
};
export const computeParameterComponents = (): Record<

View File

@ -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"

View File

@ -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');

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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(
@ -141,8 +140,8 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
`INSERT INTO ${dataSourceMetadata.schema}."viewField"
("fieldMetadataId", "position", "isVisible", "size", "viewId")
VALUES ('${createdFieldMetadata.id}', '${lastPosition + 1}', true, 180, '${
view[0].id
}')`,
view[0].id
}')`,
);
return createdFieldMetadata;

View File

@ -56,8 +56,8 @@ type DefaultValueByFieldMetadata<T extends FieldMetadataType | 'default'> = [
] extends [keyof FieldMetadataDefaultValueMapping]
? FieldMetadataDefaultValueMapping[T] | null
: T extends 'default'
? AllFieldMetadataDefaultValueTypes | null
: never;
? AllFieldMetadataDefaultValueTypes | null
: never;
export type FieldMetadataDefaultValue<
T extends FieldMetadataType | 'default' = 'default',
@ -68,10 +68,10 @@ type FieldMetadataDefaultValueExtractNestedType<T> = T extends {
}
? U
: T extends object
? { [K in keyof T]: T[K] } extends { value: infer V }
? V
: T[keyof T]
: never;
? { [K in keyof T]: T[K] } extends { value: infer V }
? V
: T[keyof T]
: never;
type FieldMetadataDefaultValueExtractedTypes = {
[K in keyof FieldMetadataDefaultValueMapping]: FieldMetadataDefaultValueExtractNestedType<

View File

@ -14,8 +14,8 @@ type OptionsByFieldMetadata<T extends FieldMetadataType | 'default'> =
T extends keyof FieldMetadataOptionsMapping
? FieldMetadataOptionsMapping[T]
: T extends 'default'
? FieldMetadataDefaultOptions[] | FieldMetadataComplexOptions[]
: never;
? FieldMetadataDefaultOptions[] | FieldMetadataComplexOptions[]
: never;
export type FieldMetadataOptions<
T extends FieldMetadataType | 'default' = 'default',

View File

@ -34,8 +34,8 @@ type TypeByFieldMetadata<T extends FieldMetadataType | 'default'> = [
] extends [keyof FieldMetadataTypeMapping]
? FieldMetadataTypeMapping[T]
: T extends 'default'
? AllFieldMetadataTypes
: FieldMetadataTargetColumnMapValue;
? AllFieldMetadataTypes
: FieldMetadataTargetColumnMapValue;
export type FieldMetadataTargetColumnMap<
T extends FieldMetadataType | 'default' = 'default',

View File

@ -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"
@ -300,8 +299,8 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
`INSERT INTO ${dataSourceMetadata.schema}."viewField"
("fieldMetadataId", "position", "isVisible", "size", "viewId")
VALUES ('${field.id}', '${index - 1}', true, 180, '${
view[0].id
}') RETURNING *`,
view[0].id
}') RETURNING *`,
);
});

View File

@ -100,11 +100,14 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
},
);
const objectMetadataMap = objectMetadataEntries.reduce((acc, curr) => {
acc[curr.id] = curr;
const objectMetadataMap = objectMetadataEntries.reduce(
(acc, curr) => {
acc[curr.id] = curr;
return acc;
}, {} as { [key: string]: ObjectMetadataEntity });
return acc;
},
{} as { [key: string]: ObjectMetadataEntity },
);
if (
objectMetadataMap[relationMetadataInput.fromObjectMetadataId] ===

View File

@ -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,
};

View File

@ -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;
}),

View File

@ -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');

View File

@ -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');

View File

@ -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(

View File

@ -146,8 +146,8 @@ export class WorkspaceMigrationEnumService {
UPDATE "${schemaName}"."${tableName}"
SET "${columnDefinition.columnName}" = ${defaultValue}
WHERE "${columnDefinition.columnName}" NOT IN (${enumValues
.map((e) => `'${e}'`)
.join(', ')})
.map((e) => `'${e}'`)
.join(', ')})
`);
}

View File

@ -38,11 +38,11 @@ export class CreateManyQueryFactory {
insertInto${
options.targetTableName
}Collection(objects: ${stringifyWithoutKeyQuote(
computedArgs.data.map((datum) => ({
id: uuidv4(),
...datum,
})),
)}) {
computedArgs.data.map((datum) => ({
id: uuidv4(),
...datum,
})),
)}) {
affectedCount
records {
${fieldsString}

View File

@ -25,8 +25,8 @@ export class DeleteManyQueryFactory {
deleteFrom${
options.targetTableName
}Collection(filter: ${stringifyWithoutKeyQuote(
args.filter,
)}, atMost: 30) {
args.filter,
)}, atMost: 30) {
affectedCount
records {
${fieldsString}

View File

@ -38,8 +38,8 @@ export class FindManyQueryFactory {
return `
query {
${options.targetTableName}Collection${
argsString ? `(${argsString})` : ''
} {
argsString ? `(${argsString})` : ''
} {
${fieldsString}
}
}

View File

@ -32,8 +32,8 @@ export class FindOneQueryFactory {
return `
query {
${options.targetTableName}Collection${
argsString ? `(${argsString})` : ''
} {
argsString ? `(${argsString})` : ''
} {
edges {
node {
${fieldsString}

View File

@ -105,8 +105,8 @@ export class RelationFieldAliasFactory {
return `
${fieldKey}: ${referencedObjectMetadata.targetTableName}Collection${
argsString ? `(${argsString})` : ''
} {
argsString ? `(${argsString})` : ''
} {
${fieldsString}
}
`;

View File

@ -41,8 +41,8 @@ export class UpdateOneQueryFactory {
update${
options.targetTableName
}Collection(set: ${stringifyWithoutKeyQuote(
argsData,
)}, filter: { id: { eq: "${computedArgs.id}" } }) {
argsData,
)}, filter: { id: { eq: "${computedArgs.id}" } }) {
affectedCount
records {
${fieldsString}

View File

@ -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) => {
acc[enumOption.value] = {
value: enumOption.value,
description: enumOption.label,
};
values: enumOptions.reduce(
(acc, enumOption) => {
acc[enumOption.value] = {
value: enumOption.value,
description: enumOption.label,
};
return acc;
}, {} as { [key: string]: { value: string; description: string } }),
return acc;
},
{} as { [key: string]: { value: string; description: string } },
),
});
}
}

View File

@ -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

View File

@ -36,18 +36,24 @@ export const mapObjectMetadataByUniqueIdentifier = <
>(
arr: T[],
): Record<string, Omit<T, 'fields'> & { fields: Record<string, U> }> => {
return arr.reduce((acc, curr) => {
acc[curr.nameSingular] = {
...curr,
fields: curr.fields.reduce((acc, curr) => {
acc[curr.name] = curr;
return arr.reduce(
(acc, curr) => {
acc[curr.nameSingular] = {
...curr,
fields: curr.fields.reduce(
(acc, curr) => {
acc[curr.name] = curr;
return acc;
}, {} as Record<string, U>),
};
return acc;
},
{} as Record<string, U>,
),
};
return acc;
}, {} as Record<string, Omit<T, 'fields'> & { fields: Record<string, U> }>);
return acc;
},
{} as Record<string, Omit<T, 'fields'> & { fields: Record<string, U> }>,
);
};
export const convertStringifiedFieldsToJSON = <

Some files were not shown because too many files have changed in this diff Show More