Skip to content

Commit 89a1b05

Browse files
committed
docs: add cursor rules for project conventions
- project-infrastructure: tooling setup decisions - typescript-conventions: coding patterns - eslint-plugin-patterns: rule implementation patterns - git-conventions: commit style guidelines
1 parent 2ab6854 commit 89a1b05

File tree

4 files changed

+764
-0
lines changed

4 files changed

+764
-0
lines changed
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
---
2+
description: Patterns for implementing ESLint rules with TypeScript
3+
globs: ["src/rules/**/*.ts"]
4+
alwaysApply: false
5+
---
6+
7+
# ESLint Plugin Development Patterns
8+
9+
## Rule Structure
10+
11+
### Using @typescript-eslint/utils
12+
13+
```ts
14+
import { ESLintUtils, type TSESTree } from "@typescript-eslint/utils";
15+
16+
const createRule = ESLintUtils.RuleCreator(
17+
(name) => `https://github.com/your-org/eslint-plugin-lingui/docs/rules/${name}.md`
18+
);
19+
20+
export const noComplexExpressions = createRule({
21+
name: "no-complex-expressions-in-message",
22+
meta: {
23+
type: "problem",
24+
docs: {
25+
description: "Disallow complex expressions in Lingui messages",
26+
},
27+
messages: {
28+
complexExpression: "Complex expression '{{expression}}' in Lingui message",
29+
},
30+
schema: [
31+
{
32+
type: "object",
33+
properties: {
34+
allowedCallees: {
35+
type: "array",
36+
items: { type: "string" },
37+
},
38+
},
39+
additionalProperties: false,
40+
},
41+
],
42+
},
43+
defaultOptions: [{ allowedCallees: ["i18n.number", "i18n.date"] }],
44+
create(context) {
45+
return {
46+
TaggedTemplateExpression(node) {
47+
// Rule implementation
48+
},
49+
};
50+
},
51+
});
52+
```
53+
54+
## Type-Aware Rules
55+
56+
### Accessing TypeScript Services
57+
58+
```ts
59+
import { ESLintUtils } from "@typescript-eslint/utils";
60+
61+
create(context) {
62+
// Get parser services (may or may not have type info)
63+
const parserServices = ESLintUtils.getParserServices(context, true);
64+
65+
// Check if type info is available
66+
const hasTypeInfo = parserServices.program !== undefined;
67+
68+
if (hasTypeInfo) {
69+
const typeChecker = parserServices.program.getTypeChecker();
70+
// Use type-aware logic
71+
} else {
72+
// Fall back to syntax-only checks
73+
}
74+
}
75+
```
76+
77+
### Getting Types from Nodes
78+
79+
```ts
80+
function getTypeOfNode(
81+
node: TSESTree.Node,
82+
parserServices: ParserServicesWithTypeInformation
83+
): ts.Type {
84+
const typeChecker = parserServices.program.getTypeChecker();
85+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
86+
return typeChecker.getTypeAtLocation(tsNode);
87+
}
88+
```
89+
90+
### Checking String Literal Types
91+
92+
```ts
93+
function isStringLiteralType(type: ts.Type): boolean {
94+
// Check for single string literal
95+
if (type.isStringLiteral()) {
96+
return true;
97+
}
98+
99+
// Check for union of string literals
100+
if (type.isUnion()) {
101+
return type.types.every((t) => t.isStringLiteral());
102+
}
103+
104+
return false;
105+
}
106+
```
107+
108+
## Common Patterns
109+
110+
### Detecting Lingui Macros
111+
112+
```ts
113+
const LINGUI_MACROS = ["t", "Trans", "msg", "defineMessage"] as const;
114+
115+
function isLinguiTaggedTemplate(node: TSESTree.TaggedTemplateExpression): boolean {
116+
return node.tag.type === "Identifier" && node.tag.name === "t";
117+
}
118+
119+
function isLinguiJSXComponent(node: TSESTree.JSXElement): boolean {
120+
const name = node.openingElement.name;
121+
return name.type === "JSXIdentifier" && name.name === "Trans";
122+
}
123+
124+
function isLinguiCallExpression(node: TSESTree.CallExpression): boolean {
125+
if (node.callee.type === "Identifier") {
126+
return ["msg", "defineMessage"].includes(node.callee.name);
127+
}
128+
return false;
129+
}
130+
```
131+
132+
### Traversing Message Content
133+
134+
```ts
135+
function getTemplateExpressions(
136+
node: TSESTree.TaggedTemplateExpression
137+
): TSESTree.Expression[] {
138+
return node.quasi.expressions;
139+
}
140+
141+
function getJSXExpressions(
142+
node: TSESTree.JSXElement
143+
): TSESTree.JSXExpressionContainer[] {
144+
return node.children.filter(
145+
(child): child is TSESTree.JSXExpressionContainer =>
146+
child.type === "JSXExpressionContainer"
147+
);
148+
}
149+
```
150+
151+
### Checking Expression Complexity
152+
153+
```ts
154+
function getMemberExpressionDepth(node: TSESTree.MemberExpression): number {
155+
let depth = 1;
156+
let current: TSESTree.Expression = node.object;
157+
158+
while (current.type === "MemberExpression") {
159+
depth++;
160+
current = current.object;
161+
}
162+
163+
return depth;
164+
}
165+
166+
function isSimpleExpression(node: TSESTree.Expression): boolean {
167+
switch (node.type) {
168+
case "Identifier":
169+
return true;
170+
case "MemberExpression":
171+
return getMemberExpressionDepth(node) <= 1;
172+
default:
173+
return false;
174+
}
175+
}
176+
```
177+
178+
## Testing Rules
179+
180+
### Using RuleTester
181+
182+
```ts
183+
import { RuleTester } from "@typescript-eslint/rule-tester";
184+
import { afterAll, describe, it } from "vitest";
185+
import { noComplexExpressions } from "./no-complex-expressions-in-message.js";
186+
187+
// Configure RuleTester to use Vitest
188+
RuleTester.afterAll = afterAll;
189+
RuleTester.describe = describe;
190+
RuleTester.it = it;
191+
192+
const ruleTester = new RuleTester({
193+
languageOptions: {
194+
parser: await import("@typescript-eslint/parser"),
195+
parserOptions: {
196+
ecmaVersion: 2022,
197+
sourceType: "module",
198+
ecmaFeatures: {
199+
jsx: true,
200+
},
201+
},
202+
},
203+
});
204+
205+
ruleTester.run("no-complex-expressions-in-message", noComplexExpressions, {
206+
valid: [
207+
`t\`Hello \${name}\``,
208+
`<Trans>Hello {name}</Trans>`,
209+
],
210+
invalid: [
211+
{
212+
code: `t\`Hello \${Math.random()}\``,
213+
errors: [{ messageId: "complexExpression" }],
214+
},
215+
],
216+
});
217+
```
218+
219+
### Type-Aware Test Cases
220+
221+
```ts
222+
const ruleTesterWithTypes = new RuleTester({
223+
languageOptions: {
224+
parser: await import("@typescript-eslint/parser"),
225+
parserOptions: {
226+
ecmaVersion: 2022,
227+
sourceType: "module",
228+
project: "./tsconfig.test.json",
229+
tsconfigRootDir: __dirname,
230+
},
231+
},
232+
});
233+
```
234+
235+
## Error Messages
236+
237+
### Using Message IDs
238+
239+
```ts
240+
messages: {
241+
complexExpression: "Avoid complex expression '{{expression}}' in translations",
242+
nestedMacro: "Nested Lingui macro '{{macro}}' is not allowed",
243+
missingText: "Translation message should contain text, not just '{{element}}'",
244+
}
245+
246+
// In rule:
247+
context.report({
248+
node,
249+
messageId: "complexExpression",
250+
data: {
251+
expression: context.sourceCode.getText(node),
252+
},
253+
});
254+
```
255+
256+
## Plugin Export
257+
258+
### Main Entry Point
259+
260+
```ts
261+
// src/index.ts
262+
import { noComplexExpressions } from "./rules/no-complex-expressions-in-message.js";
263+
import { noNestedMacros } from "./rules/no-nested-macros.js";
264+
// ... other rules
265+
266+
const plugin = {
267+
meta: {
268+
name: "eslint-plugin-lingui",
269+
version: "1.0.0",
270+
},
271+
rules: {
272+
"no-complex-expressions-in-message": noComplexExpressions,
273+
"no-nested-macros": noNestedMacros,
274+
// ... other rules
275+
},
276+
configs: {
277+
"flat/recommended": {
278+
plugins: {
279+
lingui: plugin,
280+
},
281+
rules: {
282+
"lingui/no-complex-expressions-in-message": "error",
283+
"lingui/no-nested-macros": "error",
284+
// ... other rules
285+
},
286+
},
287+
},
288+
};
289+
290+
export default plugin;
291+
```

.cursor/rules/git-conventions.mdc

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
description: Git commit conventions and workflow
3+
globs: ["**/*"]
4+
alwaysApply: false
5+
---
6+
7+
# Git Conventions
8+
9+
## Commit Style
10+
11+
### Conventional Commits
12+
13+
Use the conventional commits format:
14+
15+
```
16+
<type>(<scope>): <subject>
17+
18+
[optional body]
19+
```
20+
21+
### Types
22+
23+
- `feat`: New feature
24+
- `fix`: Bug fix
25+
- `docs`: Documentation only
26+
- `style`: Formatting, no code change
27+
- `refactor`: Code change without feature/fix
28+
- `perf`: Performance improvement
29+
- `test`: Adding/updating tests
30+
- `build`: Build system, dependencies
31+
- `ci`: CI configuration
32+
- `chore`: Maintenance tasks
33+
34+
### Commit Size
35+
36+
- **Small, atomic commits**: One logical change per commit
37+
- **Buildable**: Each commit should leave the project in a working state
38+
- **Reviewable**: Easy to understand in isolation
39+
40+
### Commit Messages
41+
42+
- **Subject line**: Imperative mood, max 72 chars, no period
43+
- **Body**: Only when subject isn't self-explanatory
44+
- **Body style**: Bullet points or short lines, no prose
45+
- **Focus**: Technical details, rationale for non-obvious decisions
46+
47+
### Examples
48+
49+
```
50+
feat(rules): add no-complex-expressions rule
51+
```
52+
53+
```
54+
build: configure typescript strict mode
55+
56+
- noUncheckedIndexedAccess for array safety
57+
- exactOptionalPropertyTypes distinguishes undefined vs missing
58+
- verbatimModuleSyntax enforces explicit type imports
59+
```
60+
61+
```
62+
fix(parser): handle missing type info gracefully
63+
64+
- parserServices.program may be undefined without tsconfig
65+
- skip type-aware checks instead of throwing
66+
```
67+
68+
### What NOT to Include in Body
69+
70+
- Obvious information repeating the subject
71+
- Prose or filler text
72+
- Implementation details visible in the diff
73+
- Generic statements like "Updated code"

0 commit comments

Comments
 (0)