Skip to content

Commit ebd2994

Browse files
authored
Merge branch 'main' into copilot/fix-pnpm-compatibility-issue
2 parents 81d239f + 2822aab commit ebd2994

File tree

1,079 files changed

+48408
-41230
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,079 files changed

+48408
-41230
lines changed
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
# ESLint Plugin Development Rules
2+
3+
When developing new ESLint rules for @db-ux/core-eslint-plugin:
4+
5+
## Rule Structure
6+
7+
- Place rule files in `packages/eslint-plugin/src/rules/{component}/` folder
8+
- Place test files in `packages/eslint-plugin/test/rules/{component}/` folder
9+
- Use standard rule format (not `ESLintUtils.RuleCreator`)
10+
- Import shared utilities from `../../shared/utils.js`
11+
- Import constants from `../../shared/constants.js`
12+
13+
## Framework Support
14+
15+
Every rule MUST support all three frameworks:
16+
17+
- **React**: PascalCase components (e.g., `<DBButton>`)
18+
- **Angular**: kebab-case with binding syntax (e.g., `<db-button [prop]="value">`)
19+
- **Vue**: PascalCase with binding syntax (e.g., `<DBButton :prop="value">`)
20+
21+
### Shared Utilities
22+
23+
- `isDBComponent(node, COMPONENTS.DBButton)` - matches both `DBButton` and `db-button`
24+
- `getAttributeValue(node, 'prop')` - matches `prop`, `[prop]`, `:prop`, and handles empty string for boolean attributes
25+
- `hasChildOfType(node, COMPONENTS.DBTooltip)` - matches both `DBTooltip` and `db-tooltip`
26+
- `createAngularVisitors(context, COMPONENTS.DBButton, handler)` - creates Angular-specific visitors with parser services
27+
- `defineTemplateBodyVisitor(context, templateVisitor, scriptVisitor)` - handles Vue, Angular, and JSX
28+
29+
### Constants
30+
31+
- Use `COMPONENTS` from `constants.js` for component names (e.g., `COMPONENTS.DBButton`)
32+
- Use `MESSAGES` from `constants.js` for error messages
33+
- Use `MESSAGE_IDS` from `constants.js` for message identifiers
34+
35+
## Critical: Attribute Value Checks
36+
37+
### Boolean Attributes
38+
39+
**ALWAYS** check for `null` instead of falsy values:
40+
41+
```typescript
42+
// ❌ WRONG - fails for empty strings and false positives
43+
if (!value) return;
44+
if (!attribute) {
45+
/* report error */
46+
}
47+
48+
// ✅ CORRECT - handles Angular boolean attributes (empty strings)
49+
if (value === null) return;
50+
if (attribute === null || attribute === "") {
51+
/* report error */
52+
}
53+
```
54+
55+
**Why**: Angular boolean attributes return `''` (empty string), which is falsy but valid.
56+
57+
### String Attributes
58+
59+
For required text attributes, check both `null` and empty string:
60+
61+
```typescript
62+
// ✅ CORRECT
63+
if (textAttribute === null || textAttribute === "") {
64+
context.report({
65+
/* error */
66+
});
67+
}
68+
```
69+
70+
### Numeric/Expression Attributes
71+
72+
For attributes that can have falsy values (0, false), only check `null`:
73+
74+
```typescript
75+
// ✅ CORRECT - allows text={0}
76+
if (text === null && !hasChildren) {
77+
context.report({
78+
/* error */
79+
});
80+
}
81+
```
82+
83+
## Critical: Element Type Checks
84+
85+
### Parent/Child Traversal
86+
87+
When checking parent or child element types, **ALWAYS** include fallback types:
88+
89+
```typescript
90+
// ❌ WRONG - misses Element$1 and Element fallbacks
91+
if (parent.type === 'JSXElement' || parent.type === 'VElement') {
92+
93+
// ✅ CORRECT - includes all possible types
94+
if (
95+
parent.type === 'JSXElement' ||
96+
parent.type === 'VElement' ||
97+
parent.type === 'Element' ||
98+
child.type === "Element$1"
99+
) {
100+
```
101+
102+
### Angular Element Types
103+
104+
Angular template parser uses both `Element` and `Element$1`:
105+
106+
```typescript
107+
// ❌ WRONG
108+
if (parent.type === 'Element' && isDBComponent(parent, COMPONENTS.DBAccordion)) {
109+
110+
// ✅ CORRECT
111+
if ((parent.type === 'Element' || parent.type === 'Element$1') && isDBComponent(parent, COMPONENTS.DBAccordion)) {
112+
```
113+
114+
### Child Element Checks
115+
116+
```typescript
117+
// ✅ CORRECT - includes all child types
118+
const iconChild = node.children?.find(
119+
(child: any) =>
120+
(child.type === "JSXElement" ||
121+
child.type === "VElement" ||
122+
child.type === "Element" ||
123+
child.type === "Element$1") &&
124+
isDBComponent(child.openingElement || child, "DBIcon")
125+
);
126+
```
127+
128+
## Rule Implementation Pattern
129+
130+
```typescript
131+
import {
132+
createAngularVisitors,
133+
defineTemplateBodyVisitor,
134+
getAttributeValue,
135+
isDBComponent
136+
} from "../../shared/utils.js";
137+
import { COMPONENTS, MESSAGES, MESSAGE_IDS } from "../../shared/constants.js";
138+
139+
export default {
140+
meta: {
141+
type: "problem" as const,
142+
docs: {
143+
description: "Rule description",
144+
url: "https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#rule-name"
145+
},
146+
fixable: "code" as const, // optional
147+
messages: {
148+
[MESSAGE_IDS.YOUR_MESSAGE_ID]: MESSAGES.YOUR_MESSAGE
149+
},
150+
schema: []
151+
},
152+
create(context: any) {
153+
// Angular handler with parser services
154+
const angularHandler = (node: any, parserServices: any) => {
155+
const value = getAttributeValue(node, "prop");
156+
// CRITICAL: Use === null for boolean checks
157+
if (value === null || value === "") {
158+
const loc = parserServices.convertNodeSourceSpanToLoc(
159+
node.sourceSpan
160+
);
161+
context.report({
162+
loc,
163+
messageId: MESSAGE_IDS.YOUR_MESSAGE_ID
164+
});
165+
}
166+
};
167+
168+
const angularVisitors = createAngularVisitors(
169+
context,
170+
COMPONENTS.DBButton,
171+
angularHandler
172+
);
173+
if (angularVisitors) return angularVisitors;
174+
175+
// React/Vue handler
176+
const checkComponent = (node: any) => {
177+
const openingElement = node.openingElement || node;
178+
if (!isDBComponent(openingElement, COMPONENTS.DBButton)) return;
179+
180+
const value = getAttributeValue(openingElement, "prop");
181+
// CRITICAL: Use === null for boolean checks
182+
if (value === null || value === "") {
183+
context.report({
184+
node: openingElement,
185+
messageId: MESSAGE_IDS.YOUR_MESSAGE_ID
186+
});
187+
}
188+
};
189+
190+
return defineTemplateBodyVisitor(
191+
context,
192+
{ VElement: checkComponent, Element: checkComponent },
193+
{ JSXElement: checkComponent }
194+
);
195+
}
196+
};
197+
```
198+
199+
## Adding New Messages
200+
201+
1. Add message to `MESSAGES` object in `src/shared/constants.ts`
202+
2. Add corresponding ID to `MESSAGE_IDS` object
203+
3. Use the constant in your rule: `[MESSAGE_IDS.YOUR_ID]: MESSAGES.YOUR_MESSAGE`
204+
205+
**NEVER** use hardcoded strings like `messageId: 'noInteractive'`
206+
207+
## Test Requirements
208+
209+
Every rule MUST have tests covering:
210+
211+
1. **Valid cases** for all three frameworks:
212+
- React example with PascalCase
213+
- Angular example with kebab-case and `[prop]="value"`
214+
- Vue example with PascalCase and `:prop="value"`
215+
216+
2. **Invalid cases** for all three frameworks:
217+
- React example
218+
- Angular example
219+
- Vue example
220+
221+
3. **Framework integration tests** in `test/frameworks/`:
222+
- Add example to `react-test.tsx`
223+
- Add example to `angular-test.html`
224+
- Add example to `vue-test.vue`
225+
- Include comment with rule name (e.g., `{/* db-ux/rule-name */}`)
226+
- Examples should demonstrate rule violations for snapshot testing
227+
228+
## Documentation Requirements
229+
230+
When adding a new rule:
231+
232+
1. Add rule to `src/index.ts` imports
233+
2. Add rule to `plugin.rules` object in `src/index.ts`
234+
3. Add rule to `recommended.rules` config in `src/index.ts`
235+
4. Update `README.md` with:
236+
- Rule name and description
237+
- Invalid examples for React, Angular, and Vue
238+
- Valid examples for React, Angular, and Vue
239+
240+
## Example Test Pattern
241+
242+
```typescript
243+
valid: [
244+
{ code: '<DBButton type="button">React</DBButton>' },
245+
{ code: '<db-button type="button">Angular</db-button>' },
246+
{ code: '<db-button [type]="buttonType">Angular</db-button>' },
247+
{ code: '<DBButton :type="buttonType">Vue</DBButton>' }
248+
],
249+
invalid: [
250+
{ code: '<DBButton>React</DBButton>', errors: [...] },
251+
{ code: '<db-button>Angular</db-button>', errors: [...] },
252+
{ code: '<DBButton>Vue</DBButton>', errors: [...] }
253+
]
254+
```
255+
256+
## Important Notes
257+
258+
- Angular boolean attributes return empty string `''` - handle with `attr.value === null || attr.value === ''`
259+
- Use `createAngularVisitors` for Angular support - it handles kebab-case conversion for components starting with `DB`
260+
- Always use `COMPONENTS` constants instead of hardcoded strings
261+
- Always use `MESSAGE_IDS` and `MESSAGES` from constants
262+
- For Angular, use `parserServices.convertNodeSourceSpanToLoc(node.sourceSpan)` for location
263+
- For React/Vue, use `node: openingElement` for location
264+
- Angular template parser uses both `Element` and `Element$1` types
265+
- Vue sometimes uses `Element` as fallback instead of `VElement`
266+
- When traversing parents/children, always check for `JSXElement`, `VElement`, and `Element` types
267+
268+
## TypeScript Type Assertions
269+
270+
All rule metadata must use `as const` assertions for TypeScript compatibility:
271+
272+
```typescript
273+
export default {
274+
meta: {
275+
type: "problem" as const, // REQUIRED: Use 'as const'
276+
fixable: "code" as const // REQUIRED if fixable is present
277+
// ...
278+
}
279+
};
280+
```
281+
282+
## Test Configuration
283+
284+
Test files should separate Angular tests from React/Vue tests:
285+
286+
```typescript
287+
import { RuleTester } from "@typescript-eslint/rule-tester";
288+
import { RuleTester as AngularRuleTester } from "@angular-eslint/test-utils";
289+
290+
const ruleTester = new RuleTester({
291+
languageOptions: {
292+
parserOptions: {
293+
ecmaFeatures: { jsx: true }
294+
}
295+
}
296+
});
297+
298+
const angularRuleTester = new AngularRuleTester();
299+
300+
describe("rule-name", () => {
301+
it("should validate rule", () => {
302+
ruleTester.run("rule-name", rule, {
303+
valid: [
304+
/* React/Vue cases */
305+
],
306+
invalid: [
307+
/* React/Vue cases */
308+
]
309+
});
310+
});
311+
312+
it("should validate rule (Angular)", () => {
313+
angularRuleTester.run("rule-name", rule, {
314+
valid: [
315+
/* Angular cases with <db- */
316+
],
317+
invalid: [
318+
/* Angular cases with <db- */
319+
]
320+
});
321+
});
322+
});
323+
```
324+
325+
## Checklist
326+
327+
Before submitting a new rule:
328+
329+
- [ ] Rule file created in `src/rules/{component}/`
330+
- [ ] Test file created in `test/rules/{component}/`
331+
- [ ] Messages added to `src/shared/constants.ts`
332+
- [ ] Rule uses `COMPONENTS` constants
333+
- [ ] Rule uses `MESSAGE_IDS` and `MESSAGES` constants (not hardcoded strings)
334+
- [ ] Rule `type` uses `as const` assertion
335+
- [ ] Rule `fixable` uses `as const` assertion (if present)
336+
- [ ] Test uses `languageOptions` configuration (not `parser` property)
337+
- [ ] Angular support via `createAngularVisitors`
338+
- [ ] Vue/React support via `defineTemplateBodyVisitor`
339+
- [ ] All attribute checks use `=== null` (not `!value`)
340+
- [ ] Required text attributes check `=== null || === ''`
341+
- [ ] Parent/child type checks include `Element` fallback
342+
- [ ] Angular type checks include both `Element` and `Element$1`
343+
- [ ] Tests include React (PascalCase) examples
344+
- [ ] Tests include Angular (kebab-case + `[prop]`) examples
345+
- [ ] Tests include Vue (PascalCase + `:prop`) examples
346+
- [ ] Framework test example added to `test/frameworks/react-test.tsx`
347+
- [ ] Framework test example added to `test/frameworks/angular-test.html`
348+
- [ ] Framework test example added to `test/frameworks/vue-test.vue`
349+
- [ ] Rule imported in `src/index.ts`
350+
- [ ] Rule added to `plugin.rules` object
351+
- [ ] Rule added to `recommended` config
352+
- [ ] README.md updated with all three framework examples

.config/.jscpd.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
"**/packages/components/scripts/post-build/components.ts",
4242
"**/packages/components/src/components/**/*.spec.tsx",
4343
"**/packages/components/src/components/**/index.html",
44-
"**/packages/components/src/shared/examples/_icons.arg.types.ts",
44+
"**/packages/components/src/**/*.arg.types.ts",
45+
"**/packages/components/src/components/custom-button/custom-button.lite.tsx",
4546
"**/packages/components/src/components/checkbox/checkbox.lite.tsx",
4647
"**/packages/components/src/components/notification/notification.lite.tsx",
4748
"**/packages/components/src/components/radio/radio.lite.tsx",
@@ -61,13 +62,16 @@
6162
"**/showcases/patternhub/generated.js",
6263
"**/showcases/react-showcase/src/components/checkbox/index.tsx",
6364
"**/showcases/react-showcase/src/components/notification/index.tsx",
64-
"**/showcases/react-showcase/src/components/notification/index.tsx",
65-
"**/showcases/react-showcase/src/components/radio/index.tsx",
6665
"**/showcases/react-showcase/src/components/radio/index.tsx",
6766
"**/showcases/shared/*.json",
6867
"**/showcases/vue-showcase/src/components/form/Form.vue",
6968
"**/CHANGELOG.md",
70-
"**/packages/components/src/shared/showcase/*-wrapper.showcase.lite.tsx"
69+
"**/packages/components/src/shared/showcase/*-wrapper.showcase.lite.tsx",
70+
"**/showcases/react-showcase/src/app.tsx",
71+
"**/showcases/react-showcase/src/utils/navigation-item.tsx",
72+
"**/playwright-report/**/*",
73+
"**/examples/**/*.example.lite.tsx",
74+
"**/packages/eslint-plugin/src/rules/**"
7175
],
7276
"ignorePattern": ["<option value=\"test5\">Test5</option>"],
7377
"absolute": true

.config/.lintstagedrc.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,5 @@ export default {
1313
() => 'npm install --package-lock-only --ignore-scripts',
1414
'npm run lint:package-json'
1515
],
16-
'*.{md,mdx,txt,yml,yaml,ts,tsx,js,jsx,html,css,scss,sass,vue}': [
17-
() => 'npm run lint:codespell'
18-
]
16+
'*': 'cspell --config .config/cspell.config.ts'
1917
};

0 commit comments

Comments
 (0)