Fix docker install (#2925)
* Fix docker install * Move back twenty-eslint-plugin to eslint-plugin-twenty * fix: add bundled yarn * Improve makeifle structure * Update commands and doc * Add pg_graphql binaries * Fix --------- Co-authored-by: Jérémy Magrin <jeremy.magrin@gmail.com>
This commit is contained in:
@ -0,0 +1,84 @@
|
||||
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;
|
||||
166
packages/eslint-plugin-twenty/src/rules/effect-components.ts
Normal file
166
packages/eslint-plugin-twenty/src/rules/effect-components.ts
Normal file
@ -0,0 +1,166 @@
|
||||
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;
|
||||
@ -0,0 +1,142 @@
|
||||
import {
|
||||
AST_NODE_TYPES,
|
||||
ESLintUtils,
|
||||
TSESTree,
|
||||
} from "@typescript-eslint/utils";
|
||||
|
||||
const createRule = ESLintUtils.RuleCreator(() => `https://docs.twenty.com`);
|
||||
|
||||
const matchingStateVariableRule = createRule({
|
||||
create: (context) => {
|
||||
return {
|
||||
VariableDeclarator: (node: TSESTree.VariableDeclarator) => {
|
||||
if (
|
||||
node?.init?.type === AST_NODE_TYPES.CallExpression &&
|
||||
node.init.callee.type === AST_NODE_TYPES.Identifier &&
|
||||
[
|
||||
"useRecoilState",
|
||||
"useRecoilScopedState",
|
||||
"useRecoilFamilyState",
|
||||
"useRecoilScopedFamilyState",
|
||||
"useRecoilValue",
|
||||
"useRecoilScopedValue",
|
||||
].includes(node.init.callee.name)
|
||||
) {
|
||||
const stateNameBase =
|
||||
node.init.arguments?.[0]?.type === AST_NODE_TYPES.Identifier
|
||||
? node.init.arguments[0].name
|
||||
: undefined;
|
||||
|
||||
if (!stateNameBase) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expectedVariableNameBase = stateNameBase.replace(
|
||||
/(State|FamilyState|Selector|ScopedState|ScopedFamilyState|ScopedSelector)$/,
|
||||
"",
|
||||
);
|
||||
|
||||
if (node.id.type === AST_NODE_TYPES.Identifier) {
|
||||
const actualVariableName = node.id.name;
|
||||
if (actualVariableName !== expectedVariableNameBase) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "invalidVariableName",
|
||||
data: {
|
||||
actualName: actualVariableName,
|
||||
expectedName: expectedVariableNameBase,
|
||||
hookName: stateNameBase,
|
||||
callee: node.init.callee.name,
|
||||
},
|
||||
fix: (fixer) => {
|
||||
return fixer.replaceText(node.id, expectedVariableNameBase);
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.id.type === AST_NODE_TYPES.ArrayPattern) {
|
||||
const actualVariableName =
|
||||
node.id.elements?.[0]?.type === AST_NODE_TYPES.Identifier
|
||||
? node.id.elements[0].name
|
||||
: undefined;
|
||||
if (
|
||||
actualVariableName &&
|
||||
actualVariableName !== expectedVariableNameBase
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "invalidVariableName",
|
||||
data: {
|
||||
actual: actualVariableName,
|
||||
expected: expectedVariableNameBase,
|
||||
callee: node.init.callee.name,
|
||||
},
|
||||
fix: (fixer) => {
|
||||
if (node.id.type === AST_NODE_TYPES.ArrayPattern) {
|
||||
return fixer.replaceText(
|
||||
node.id.elements[0] as TSESTree.Node,
|
||||
expectedVariableNameBase,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (node.id.elements?.[1]?.type === AST_NODE_TYPES.Identifier) {
|
||||
const actualSetterName = node.id.elements[1].name;
|
||||
const expectedSetterName = `set${expectedVariableNameBase
|
||||
.charAt(0)
|
||||
.toUpperCase()}${expectedVariableNameBase.slice(1)}`;
|
||||
|
||||
if (actualSetterName !== expectedSetterName) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "invalidSetterName",
|
||||
data: {
|
||||
hookName: stateNameBase,
|
||||
actualName: actualSetterName,
|
||||
expectedName: expectedSetterName,
|
||||
},
|
||||
fix: (fixer) => {
|
||||
if (node.id.type === AST_NODE_TYPES.ArrayPattern) {
|
||||
return fixer.replaceText(
|
||||
node.id.elements[1]!,
|
||||
expectedSetterName,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
name: "recoil-hook-naming",
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Ensure recoil value and setter are named after their atom name",
|
||||
recommended: "recommended",
|
||||
},
|
||||
fixable: "code",
|
||||
schema: [],
|
||||
messages: {
|
||||
invalidVariableName:
|
||||
"Invalid usage of {{ hookName }}: the variable should be named '{{ expectedName }}' but found '{{ actualName }}'.",
|
||||
invalidSetterName:
|
||||
"Invalid usage of {{ hookName }}: Expected setter '{{ expectedName }}' but found '{{ actualName }}'.",
|
||||
},
|
||||
},
|
||||
defaultOptions: [],
|
||||
});
|
||||
|
||||
module.exports = matchingStateVariableRule;
|
||||
|
||||
export default matchingStateVariableRule;
|
||||
@ -0,0 +1,64 @@
|
||||
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
|
||||
|
||||
const createRule = ESLintUtils.RuleCreator(() => `https://docs.twenty.com`);
|
||||
|
||||
const noHardcodedColorsRule = createRule({
|
||||
create: (context) => {
|
||||
const testHardcodedColor = (
|
||||
literal: TSESTree.Literal | TSESTree.TemplateLiteral,
|
||||
) => {
|
||||
const colorRegex = /(?:rgba?\()|(?:#[0-9a-fA-F]{2,6})/i;
|
||||
|
||||
if (
|
||||
literal.type === TSESTree.AST_NODE_TYPES.Literal &&
|
||||
typeof literal.value === "string"
|
||||
) {
|
||||
if (colorRegex.test(literal.value)) {
|
||||
context.report({
|
||||
node: literal,
|
||||
messageId: "hardcodedColor",
|
||||
data: {
|
||||
color: literal.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (literal.type === TSESTree.AST_NODE_TYPES.TemplateLiteral) {
|
||||
const firstStringValue = literal.quasis[0]?.value.raw;
|
||||
|
||||
if (colorRegex.test(firstStringValue)) {
|
||||
context.report({
|
||||
node: literal,
|
||||
messageId: "hardcodedColor",
|
||||
data: {
|
||||
color: firstStringValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
Literal: testHardcodedColor,
|
||||
TemplateLiteral: testHardcodedColor,
|
||||
};
|
||||
},
|
||||
name: "no-hardcoded-colors",
|
||||
meta: {
|
||||
docs: {
|
||||
description:
|
||||
"Do not use hardcoded RGBA or Hex colors. Please use a color from the theme file.",
|
||||
},
|
||||
messages: {
|
||||
hardcodedColor:
|
||||
"Hardcoded color {{ color }} found. Please use a color from the theme file.",
|
||||
},
|
||||
type: "suggestion",
|
||||
schema: [],
|
||||
fixable: "code",
|
||||
},
|
||||
defaultOptions: [],
|
||||
});
|
||||
|
||||
module.exports = noHardcodedColorsRule;
|
||||
|
||||
export default noHardcodedColorsRule;
|
||||
68
packages/eslint-plugin-twenty/src/rules/no-state-useref.ts
Normal file
68
packages/eslint-plugin-twenty/src/rules/no-state-useref.ts
Normal file
@ -0,0 +1,68 @@
|
||||
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;
|
||||
@ -0,0 +1,298 @@
|
||||
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;
|
||||
@ -0,0 +1,62 @@
|
||||
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;
|
||||
@ -0,0 +1,47 @@
|
||||
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>;",
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -0,0 +1,85 @@
|
||||
import { RuleTester } from "@typescript-eslint/rule-tester";
|
||||
|
||||
import effectComponentsRule from "../rules/effect-components";
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run("effect-components", effectComponentsRule, {
|
||||
valid: [
|
||||
{
|
||||
code: `const TestComponentEffect = () => <></>;`,
|
||||
},
|
||||
{
|
||||
code: `const TestComponent = () => <div></div>;`,
|
||||
},
|
||||
{
|
||||
code: `export const useUpdateEffect = () => null;`,
|
||||
},
|
||||
{
|
||||
code: `export const useUpdateEffect = () => <></>;`,
|
||||
},
|
||||
{
|
||||
code: `const TestComponent = () => <><div></div></>;`,
|
||||
},
|
||||
{
|
||||
code: `const TestComponentEffect = () => null;`,
|
||||
},
|
||||
{
|
||||
code: `const TestComponentEffect = () => {
|
||||
useEffect(() => {}, []);
|
||||
|
||||
return null;
|
||||
}`,
|
||||
},
|
||||
{
|
||||
code: `const TestComponentEffect = () => {
|
||||
useEffect(() => {}, []);
|
||||
|
||||
return <></>;
|
||||
}`,
|
||||
},
|
||||
{
|
||||
code: `const TestComponentEffect = () => {
|
||||
useEffect(() => {}, []);
|
||||
|
||||
return <></>;
|
||||
}`,
|
||||
},
|
||||
{
|
||||
code: `const TestComponentEffect = () => {
|
||||
useEffect(() => {}, []);
|
||||
|
||||
return null;
|
||||
}`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "const TestComponent = () => <></>;",
|
||||
output: "const TestComponentEffect = () => <></>;",
|
||||
errors: [
|
||||
{
|
||||
messageId: "effectSuffix",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "const TestComponentEffect = () => <><div></div></>;",
|
||||
output: "const TestComponent = () => <><div></div></>;",
|
||||
errors: [
|
||||
{
|
||||
messageId: "noEffectSuffix",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
1
packages/eslint-plugin-twenty/src/tests/file.ts
Normal file
1
packages/eslint-plugin-twenty/src/tests/file.ts
Normal file
@ -0,0 +1 @@
|
||||
// Required by typescript-eslint https://typescript-eslint.io/packages/rule-tester#type-aware-testing
|
||||
@ -0,0 +1,185 @@
|
||||
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);",
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -0,0 +1,62 @@
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -0,0 +1,51 @@
|
||||
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
packages/eslint-plugin-twenty/src/tests/react.tsx
Normal file
1
packages/eslint-plugin-twenty/src/tests/react.tsx
Normal file
@ -0,0 +1 @@
|
||||
// Required by typescript-eslint https://typescript-eslint.io/packages/rule-tester#type-aware-testing
|
||||
@ -0,0 +1,56 @@
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
@ -0,0 +1,47 @@
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
6
packages/eslint-plugin-twenty/src/tests/tsconfig.json
Normal file
6
packages/eslint-plugin-twenty/src/tests/tsconfig.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
},
|
||||
"include": ["./file.ts", "./react.tsx"]
|
||||
}
|
||||
Reference in New Issue
Block a user