pasika 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -55
- package/dist/eslint/pasika/index.d.ts +18 -0
- package/dist/eslint/pasika/index.js +18 -3
- package/dist/eslint/pasika/rules/enforce-barrel-exports.d.ts +9 -0
- package/dist/eslint/pasika/rules/enforce-barrel-exports.js +78 -0
- package/dist/eslint/pasika/rules/enforce-cn-merge.d.ts +9 -0
- package/dist/eslint/pasika/rules/enforce-cn-merge.js +84 -0
- package/dist/eslint/pasika/rules/enforce-cva-variant-props.d.ts +9 -0
- package/dist/eslint/pasika/rules/enforce-cva-variant-props.js +76 -0
- package/dist/eslint/pasika/rules/filename-case.js +10 -0
- package/dist/eslint/pasika/rules/import-boundaries.d.ts +2 -0
- package/dist/eslint/pasika/rules/{organization-imports.js → import-boundaries.js} +11 -1
- package/dist/eslint/pasika/rules/no-arbitrary-tailwind.d.ts +9 -0
- package/dist/eslint/pasika/rules/no-arbitrary-tailwind.js +107 -0
- package/dist/eslint/pasika/rules/no-mixed-concerns.d.ts +9 -0
- package/dist/eslint/pasika/rules/no-mixed-concerns.js +84 -0
- package/package.json +5 -16
- package/claude/hooks/.vulyk +0 -3
- package/claude/hooks/AGENTS.md +0 -3
- package/claude/hooks/CLAUDE.md +0 -1
- package/claude/hooks/claude-hooks.md +0 -30
- package/claude/hooks/notification.sh +0 -38
- package/claude/hooks/protect-files.sh +0 -21
- package/claude/hooks/status-line/index.js +0 -57
- package/claude/scripts/render-settings.ts +0 -223
- package/claude/settings.base.json +0 -38
- package/dist/claude/scripts/render-settings.js +0 -145
- package/dist/eslint/pasika/rules/organization-imports.d.ts +0 -2
- package/dist/eslint.config.js +0 -7
- package/dist/scripts/pasika.js +0 -59
- package/docs/agent-conventions.md +0 -27
- package/docs/claude/hooks.md +0 -30
- package/docs/code-organization-guide/code-organization-guide.md +0 -69
- package/docs/code-organization-guide/references/application-architecture-reference.md +0 -149
- package/docs/code-organization-guide/rules/component-placement-rule.md +0 -119
- package/docs/code-organization-guide/rules/configuration-rule.md +0 -42
- package/docs/code-organization-guide/rules/constants-rule.md +0 -74
- package/docs/code-organization-guide/rules/exports-and-imports-rule.md +0 -84
- package/docs/code-organization-guide/rules/folder-nesting-rule.md +0 -82
- package/docs/code-organization-guide/rules/hook-extraction-rule.md +0 -141
- package/docs/code-organization-guide/rules/interactive-component-rule.md +0 -97
- package/docs/code-organization-guide/rules/jsx-hygiene-rule.md +0 -67
- package/docs/code-organization-guide/rules/locales-rule.md +0 -53
- package/docs/code-organization-guide/rules/nameable-visual-concept-rule.md +0 -66
- package/docs/code-organization-guide/rules/no-mixed-concerns-rule.md +0 -63
- package/docs/code-organization-guide/rules/repeated-structure-rule.md +0 -93
- package/docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md +0 -112
- package/docs/code-organization-guide/rules/sole-state-owner-rule.md +0 -101
- package/docs/code-organization-guide/rules/types-and-schemas-rule.md +0 -139
- package/docs/code-organization-guide/rules/utilities-rule.md +0 -86
- package/docs/documentation-guide/_templates/grouped-reference.md +0 -11
- package/docs/documentation-guide/_templates/guide.md +0 -19
- package/docs/documentation-guide/_templates/rule.md +0 -21
- package/docs/documentation-guide/_templates/single-lookup-reference.md +0 -5
- package/docs/documentation-guide/documentation-guide.md +0 -13
- package/docs/documentation-guide/references/documentation-types-reference.md +0 -9
- package/docs/documentation-guide/rules/guide-creation-rule.md +0 -113
- package/docs/documentation-guide/rules/reference-creation-rule.md +0 -132
- package/docs/documentation-guide/rules/rule-creation-rule.md +0 -81
- package/docs/documentation-guide/rules/template-usage-rule.md +0 -49
- package/docs/shadcn-theme.md +0 -121
- package/docs/styling-guide/rules/arbitrary-value-rule.md +0 -31
- package/docs/styling-guide/rules/class-composition-rule.md +0 -52
- package/docs/styling-guide/rules/component-ui-state-rule.md +0 -53
- package/docs/styling-guide/rules/component-variant-rule.md +0 -125
- package/docs/styling-guide/rules/global-stylesheet-rule.md +0 -67
- package/docs/styling-guide/rules/theme-and-utility-definition-rule.md +0 -86
- package/docs/styling-guide/styling-guide.md +0 -14
package/README.md
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
# pasika
|
|
2
2
|
|
|
3
|
-
Reusable agent setup package
|
|
3
|
+
Reusable agent setup package.
|
|
4
4
|
|
|
5
|
-
`pasika` stores portable agent setup assets that individual repos can apply or adapt.
|
|
5
|
+
`pasika` stores portable agent setup assets that individual repos can apply or adapt.
|
|
6
6
|
|
|
7
7
|
## Scope
|
|
8
8
|
|
|
9
9
|
This repo intentionally starts narrow:
|
|
10
10
|
|
|
11
|
-
- Claude first for v1
|
|
12
11
|
- reusable agent setup assets only
|
|
13
12
|
- shared docs at the repo root
|
|
14
13
|
- no project-specific workflows or business logic
|
|
@@ -16,32 +15,21 @@ This repo intentionally starts narrow:
|
|
|
16
15
|
## Layout
|
|
17
16
|
|
|
18
17
|
```text
|
|
19
|
-
claude/
|
|
20
|
-
settings.base.json
|
|
21
|
-
hooks/
|
|
22
|
-
status-line/
|
|
23
|
-
index.js
|
|
24
|
-
notification.sh
|
|
25
|
-
protect-files.sh
|
|
26
|
-
scripts/
|
|
27
|
-
render-settings.ts
|
|
28
18
|
docs/
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
code-organization-guide/
|
|
20
|
+
documentation-guide/
|
|
21
|
+
styling-guide/
|
|
22
|
+
agent-conventions.md
|
|
23
|
+
eslint/
|
|
24
|
+
pasika/
|
|
25
|
+
rules/
|
|
33
26
|
dist/
|
|
34
27
|
...
|
|
35
|
-
vulyk.json
|
|
36
|
-
AGENTS.md
|
|
37
|
-
CLAUDE.md
|
|
38
28
|
```
|
|
39
29
|
|
|
40
30
|
## What belongs here
|
|
41
31
|
|
|
42
32
|
- portable setup scripts
|
|
43
|
-
- base settings templates
|
|
44
|
-
- shared docs and generated `CLAUDE.md`
|
|
45
33
|
- shared naming and layout conventions
|
|
46
34
|
- reusable source-organization lint rules
|
|
47
35
|
|
|
@@ -52,56 +40,39 @@ CLAUDE.md
|
|
|
52
40
|
- repository-specific plugin choices
|
|
53
41
|
- scripts that depend on a specific app, CI setup, or codebase
|
|
54
42
|
|
|
55
|
-
## CLI
|
|
56
|
-
|
|
57
|
-
The current entry point is:
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
npx pasika claude
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Optional flags:
|
|
64
|
-
|
|
65
|
-
- `--target-dir <path>` writes into another repo
|
|
66
|
-
- `--force` replaces an existing generated file instead of merging
|
|
67
|
-
|
|
68
43
|
## Development
|
|
69
44
|
|
|
70
|
-
The CLI source lives in TypeScript.
|
|
71
|
-
|
|
72
45
|
```bash
|
|
73
46
|
npm run lint
|
|
74
47
|
npm run typecheck
|
|
75
48
|
npm run build
|
|
76
49
|
```
|
|
77
50
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
## Recommended Integration
|
|
81
|
-
|
|
82
|
-
Hooks and helper executables should come from `node_modules`.
|
|
51
|
+
## ESLint Pasika Ruleset
|
|
83
52
|
|
|
84
|
-
|
|
53
|
+
Pasika ships enforceable lint rules derived from its documentation. Each rule carries a `@see` annotation linking to its source doc so future audits can verify rule/doc alignment.
|
|
85
54
|
|
|
86
|
-
|
|
87
|
-
- easy upgrades through the package manager
|
|
88
|
-
- no manual copying of hook files into every repo
|
|
55
|
+
### Usage with Zirka (recommended)
|
|
89
56
|
|
|
90
|
-
|
|
57
|
+
Enable pasika through Zirka's `styleguide`:
|
|
91
58
|
|
|
92
|
-
|
|
59
|
+
```ts
|
|
60
|
+
// eslint.config.ts
|
|
61
|
+
import { RuleSeverity, styleguide } from "zirka";
|
|
93
62
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
63
|
+
const { eslintConfig } = styleguide({
|
|
64
|
+
next: RuleSeverity.Error,
|
|
65
|
+
node: RuleSeverity.Error,
|
|
66
|
+
typescript: RuleSeverity.Error,
|
|
67
|
+
pasika: RuleSeverity.Error,
|
|
68
|
+
});
|
|
99
69
|
|
|
100
|
-
|
|
70
|
+
export default eslintConfig;
|
|
71
|
+
```
|
|
101
72
|
|
|
102
|
-
|
|
73
|
+
### Usage without Zirka
|
|
103
74
|
|
|
104
|
-
|
|
75
|
+
Import the config directly and compose with your own:
|
|
105
76
|
|
|
106
77
|
```ts
|
|
107
78
|
// eslint.config.ts
|
|
@@ -116,3 +87,17 @@ const { eslintConfig } = styleguide({
|
|
|
116
87
|
|
|
117
88
|
export default [...((await eslintConfig) ?? []), pasikaConfig];
|
|
118
89
|
```
|
|
90
|
+
|
|
91
|
+
### Rule → Doc Mapping
|
|
92
|
+
|
|
93
|
+
Every rule's source file declares the documentation it enforces. Use this table to trace a lint hit back to its reasoning:
|
|
94
|
+
|
|
95
|
+
| Rule | Enforces | Source Doc |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `pasika/filename-case` | Smart/PascalCase vs dumb/kebab-case file names | `docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md` |
|
|
98
|
+
| `pasika/import-boundaries` | Relative vs `@/*` imports, layer boundaries | `docs/code-organization-guide/rules/exports-and-imports-rule.md` |
|
|
99
|
+
| `pasika/no-mixed-concerns` | One React component per `.tsx` file | `docs/code-organization-guide/rules/no-mixed-concerns-rule.md` |
|
|
100
|
+
| `pasika/no-arbitrary-tailwind` | No arbitrary `-[value]` Tailwind classes | `docs/styling-guide/rules/arbitrary-value-rule.md` |
|
|
101
|
+
| `pasika/enforce-cn-merge` | Use `cn()` not `+`/template literals; ≤5 classes per group | `docs/styling-guide/rules/class-composition-rule.md` |
|
|
102
|
+
| `pasika/enforce-cva-variant-props` | Use `VariantProps<typeof>` not manual union types | `docs/styling-guide/rules/component-variant-rule.md` |
|
|
103
|
+
| `pasika/enforce-barrel-exports` | Nested `index.ts` only re-exports the parent component | `docs/code-organization-guide/rules/folder-nesting-rule.md` |
|
|
@@ -1,2 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pasika ESLint Ruleset
|
|
3
|
+
*
|
|
4
|
+
* Each rule enforces a specific documentation Rule from the `docs/` tree.
|
|
5
|
+
* Every rule file carries a `@see` annotation linking to the source doc.
|
|
6
|
+
* This mapping is maintained so future agents can audit rule/doc alignment.
|
|
7
|
+
*
|
|
8
|
+
* Rule → Doc mapping:
|
|
9
|
+
*
|
|
10
|
+
* filename-case → docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md
|
|
11
|
+
* import-boundaries → docs/code-organization-guide/rules/exports-and-imports-rule.md
|
|
12
|
+
* no-mixed-concerns → docs/code-organization-guide/rules/no-mixed-concerns-rule.md
|
|
13
|
+
* no-arbitrary-tailwind → docs/styling-guide/rules/arbitrary-value-rule.md
|
|
14
|
+
* enforce-cn-merge → docs/styling-guide/rules/class-composition-rule.md
|
|
15
|
+
* enforce-cva-variant-props → docs/styling-guide/rules/component-variant-rule.md
|
|
16
|
+
* enforce-barrel-exports → docs/code-organization-guide/rules/folder-nesting-rule.md
|
|
17
|
+
* + docs/code-organization-guide/rules/exports-and-imports-rule.md
|
|
18
|
+
*/
|
|
1
19
|
import type { Linter } from "eslint";
|
|
2
20
|
export declare const pasikaConfig: Linter.Config;
|
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
import { filenameCaseRule } from "./rules/filename-case.js";
|
|
2
|
-
import {
|
|
2
|
+
import { importBoundariesRule } from "./rules/import-boundaries.js";
|
|
3
|
+
import { noMixedConcernsRule } from "./rules/no-mixed-concerns.js";
|
|
4
|
+
import { noArbitraryTailwindRule } from "./rules/no-arbitrary-tailwind.js";
|
|
5
|
+
import { enforceCnMergeRule } from "./rules/enforce-cn-merge.js";
|
|
6
|
+
import { enforceCvaVariantPropsRule } from "./rules/enforce-cva-variant-props.js";
|
|
7
|
+
import { enforceBarrelExportsRule } from "./rules/enforce-barrel-exports.js";
|
|
3
8
|
export const pasikaConfig = {
|
|
4
9
|
files: ["src/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}"],
|
|
5
10
|
plugins: {
|
|
6
11
|
pasika: {
|
|
7
12
|
rules: {
|
|
8
13
|
"filename-case": filenameCaseRule,
|
|
9
|
-
"
|
|
14
|
+
"import-boundaries": importBoundariesRule,
|
|
15
|
+
"no-mixed-concerns": noMixedConcernsRule,
|
|
16
|
+
"no-arbitrary-tailwind": noArbitraryTailwindRule,
|
|
17
|
+
"enforce-cn-merge": enforceCnMergeRule,
|
|
18
|
+
"enforce-cva-variant-props": enforceCvaVariantPropsRule,
|
|
19
|
+
"enforce-barrel-exports": enforceBarrelExportsRule,
|
|
10
20
|
},
|
|
11
21
|
},
|
|
12
22
|
},
|
|
13
23
|
rules: {
|
|
14
24
|
"pasika/filename-case": "error",
|
|
15
|
-
"pasika/
|
|
25
|
+
"pasika/import-boundaries": "error",
|
|
26
|
+
"pasika/no-mixed-concerns": "error",
|
|
27
|
+
"pasika/no-arbitrary-tailwind": "error",
|
|
28
|
+
"pasika/enforce-cn-merge": "error",
|
|
29
|
+
"pasika/enforce-cva-variant-props": "error",
|
|
30
|
+
"pasika/enforce-barrel-exports": "error",
|
|
16
31
|
},
|
|
17
32
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/enforce-barrel-exports
|
|
3
|
+
*
|
|
4
|
+
* Enforces the "Folder Nesting Rule" — index.ts only re-exports parent component.
|
|
5
|
+
*
|
|
6
|
+
* @see docs/code-organization-guide/rules/folder-nesting-rule.md
|
|
7
|
+
*/
|
|
8
|
+
import type { Rule } from "eslint";
|
|
9
|
+
export declare const enforceBarrelExportsRule: Rule.RuleModule;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/enforce-barrel-exports
|
|
3
|
+
*
|
|
4
|
+
* Enforces the "Folder Nesting Rule" — index.ts only re-exports parent component.
|
|
5
|
+
*
|
|
6
|
+
* @see docs/code-organization-guide/rules/folder-nesting-rule.md
|
|
7
|
+
*/
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
function isPascalCase(str) {
|
|
11
|
+
return /^[A-Z][A-Za-z0-9]*$/.test(str);
|
|
12
|
+
}
|
|
13
|
+
function isKebabCase(str) {
|
|
14
|
+
return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(str);
|
|
15
|
+
}
|
|
16
|
+
const SUPPORT_FOLDERS = new Set(["types", "schemas", "hooks", "constants", "utils", "config", "locales"]);
|
|
17
|
+
export const enforceBarrelExportsRule = {
|
|
18
|
+
meta: {
|
|
19
|
+
schema: [],
|
|
20
|
+
type: "problem",
|
|
21
|
+
docs: {
|
|
22
|
+
description: "Enforce index.ts barrels only re-export the parent component.",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
create(context) {
|
|
26
|
+
const filename = context.filename;
|
|
27
|
+
if (!filename)
|
|
28
|
+
return {};
|
|
29
|
+
const baseName = path.basename(filename);
|
|
30
|
+
if (baseName !== "index.ts" && baseName !== "index.cts" && baseName !== "index.mts")
|
|
31
|
+
return {};
|
|
32
|
+
const dirPath = path.dirname(filename);
|
|
33
|
+
const folderName = path.basename(dirPath);
|
|
34
|
+
const parentFolderName = path.basename(path.dirname(dirPath));
|
|
35
|
+
if (SUPPORT_FOLDERS.has(folderName))
|
|
36
|
+
return {};
|
|
37
|
+
if (!isPascalCase(folderName) && !isKebabCase(folderName))
|
|
38
|
+
return {};
|
|
39
|
+
const matchingTsx = fs.existsSync(path.join(dirPath, `${folderName}.tsx`)) ? folderName : null;
|
|
40
|
+
if (!matchingTsx)
|
|
41
|
+
return {};
|
|
42
|
+
if (!isPascalCase(parentFolderName) && !isKebabCase(parentFolderName))
|
|
43
|
+
return {};
|
|
44
|
+
const reExportedNames = new Set();
|
|
45
|
+
return {
|
|
46
|
+
ExportNamedDeclaration(node) {
|
|
47
|
+
if (!node.source)
|
|
48
|
+
return;
|
|
49
|
+
for (const spec of node.specifiers) {
|
|
50
|
+
if (spec.exported.type === "Identifier") {
|
|
51
|
+
reExportedNames.add(spec.exported.name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"Program:exit"() {
|
|
56
|
+
if (reExportedNames.size === 0)
|
|
57
|
+
return;
|
|
58
|
+
if (!reExportedNames.has(matchingTsx)) {
|
|
59
|
+
context.report({
|
|
60
|
+
loc: { line: 1, column: 0 },
|
|
61
|
+
message: `index.ts in "${folderName}/" must re-export "${matchingTsx}". ` +
|
|
62
|
+
"See docs/code-organization-guide/rules/folder-nesting-rule.md",
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const nonParentExports = [...reExportedNames].filter((n) => n !== matchingTsx);
|
|
67
|
+
if (nonParentExports.length > 0) {
|
|
68
|
+
context.report({
|
|
69
|
+
loc: { line: 1, column: 0 },
|
|
70
|
+
message: `index.ts must not re-export exclusive children: ${nonParentExports.join(", ")}. ` +
|
|
71
|
+
`Only "${matchingTsx}" may be re-exported. ` +
|
|
72
|
+
"See docs/code-organization-guide/rules/folder-nesting-rule.md",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/enforce-cn-merge
|
|
3
|
+
*
|
|
4
|
+
* Enforces the "Class Composition Rule".
|
|
5
|
+
*
|
|
6
|
+
* @see docs/styling-guide/rules/class-composition-rule.md
|
|
7
|
+
*/
|
|
8
|
+
function classCount(str) {
|
|
9
|
+
return str.split(/\s+/).filter(Boolean).length;
|
|
10
|
+
}
|
|
11
|
+
export const enforceCnMergeRule = {
|
|
12
|
+
meta: {
|
|
13
|
+
schema: [],
|
|
14
|
+
type: "problem",
|
|
15
|
+
docs: {
|
|
16
|
+
description: "Enforce cn() for conditional class merging.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
create(context) {
|
|
20
|
+
return {
|
|
21
|
+
JSXAttribute(node) {
|
|
22
|
+
if (node.name.name !== "className" && node.name.name !== "class")
|
|
23
|
+
return;
|
|
24
|
+
const valueNode = node.value;
|
|
25
|
+
if (!valueNode)
|
|
26
|
+
return;
|
|
27
|
+
if (valueNode.type === "Literal" && typeof valueNode.value === "string") {
|
|
28
|
+
if (classCount(valueNode.value) > 5) {
|
|
29
|
+
context.report({
|
|
30
|
+
node,
|
|
31
|
+
message: "Static className with more than 5 classes must use cn() with grouped string literals. " +
|
|
32
|
+
"See docs/styling-guide/rules/class-composition-rule.md",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (valueNode.type !== "JSXExpressionContainer" || !valueNode.expression)
|
|
38
|
+
return;
|
|
39
|
+
const expr = valueNode.expression;
|
|
40
|
+
if (expr.type === "BinaryExpression" && expr.operator === "+") {
|
|
41
|
+
context.report({
|
|
42
|
+
node,
|
|
43
|
+
message: "Use cn() instead of + operator for className. " +
|
|
44
|
+
"See docs/styling-guide/rules/class-composition-rule.md",
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (expr.type === "TemplateLiteral" && expr.expressions.length > 0) {
|
|
49
|
+
context.report({
|
|
50
|
+
node,
|
|
51
|
+
message: "Use cn() instead of template literals with conditionals for className. " +
|
|
52
|
+
"See docs/styling-guide/rules/class-composition-rule.md",
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (expr.type === "Literal" && typeof expr.value === "string") {
|
|
57
|
+
if (classCount(expr.value) > 5) {
|
|
58
|
+
context.report({
|
|
59
|
+
node,
|
|
60
|
+
message: "Static className with more than 5 classes must use cn() with grouped string literals. " +
|
|
61
|
+
"See docs/styling-guide/rules/class-composition-rule.md",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (expr.type === "CallExpression" && expr.callee.type === "Identifier" && expr.callee.name === "cn") {
|
|
67
|
+
for (const arg of expr.arguments) {
|
|
68
|
+
if (arg.type === "Literal" && typeof arg.value === "string") {
|
|
69
|
+
if (classCount(arg.value) > 5) {
|
|
70
|
+
context.report({
|
|
71
|
+
node: arg,
|
|
72
|
+
message: "Each cn() string argument must contain at most 5 class names. " +
|
|
73
|
+
"Group by styling concern. " +
|
|
74
|
+
"See docs/styling-guide/rules/class-composition-rule.md",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
/* eslint-enable -- re-enable rules disabled for AST access @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument */
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/enforce-cva-variant-props
|
|
3
|
+
*
|
|
4
|
+
* Enforces the "Component Variant Rule" — VariantProps from CVA.
|
|
5
|
+
*
|
|
6
|
+
* @see docs/styling-guide/rules/component-variant-rule.md
|
|
7
|
+
*/
|
|
8
|
+
import type { Rule } from "eslint";
|
|
9
|
+
export declare const enforceCvaVariantPropsRule: Rule.RuleModule;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/enforce-cva-variant-props
|
|
3
|
+
*
|
|
4
|
+
* Enforces the "Component Variant Rule" — VariantProps from CVA.
|
|
5
|
+
*
|
|
6
|
+
* @see docs/styling-guide/rules/component-variant-rule.md
|
|
7
|
+
*/
|
|
8
|
+
export const enforceCvaVariantPropsRule = {
|
|
9
|
+
meta: {
|
|
10
|
+
schema: [],
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: {
|
|
13
|
+
description: "Enforce VariantProps from CVA instead of manual variant union types.",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
const cvaDefinitions = new Map();
|
|
18
|
+
return {
|
|
19
|
+
VariableDeclarator(node) {
|
|
20
|
+
if (node.init?.type === "CallExpression" &&
|
|
21
|
+
node.init.callee.type === "Identifier" &&
|
|
22
|
+
node.init.callee.name === "cva" &&
|
|
23
|
+
node.id.type === "Identifier") {
|
|
24
|
+
const args = node.init.arguments;
|
|
25
|
+
if (args.length < 2)
|
|
26
|
+
return;
|
|
27
|
+
const options = args[1];
|
|
28
|
+
if (options?.type === "ObjectExpression") {
|
|
29
|
+
for (const prop of options.properties) {
|
|
30
|
+
if (prop.type === "Property" &&
|
|
31
|
+
prop.key.type === "Identifier" &&
|
|
32
|
+
prop.key.name === "variants" &&
|
|
33
|
+
prop.value.type === "ObjectExpression") {
|
|
34
|
+
const variantNames = prop.value.properties
|
|
35
|
+
.filter((p) => p.type === "Property" && p.key.type === "Identifier")
|
|
36
|
+
.map((p) => p.key.name);
|
|
37
|
+
if (variantNames.length > 0) {
|
|
38
|
+
cvaDefinitions.set(node.id.name, variantNames);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
TSTypeAliasDeclaration(node) {
|
|
46
|
+
if (!node.id.name.endsWith("Props"))
|
|
47
|
+
return;
|
|
48
|
+
if (node.typeAnnotation.type !== "TSTypeLiteral")
|
|
49
|
+
return;
|
|
50
|
+
for (const member of node.typeAnnotation.members) {
|
|
51
|
+
if (member.type === "TSPropertySignature" &&
|
|
52
|
+
member.key.type === "Identifier" &&
|
|
53
|
+
member.typeAnnotation?.typeAnnotation?.type === "TSUnionType") {
|
|
54
|
+
const keyName = String(member.key.name);
|
|
55
|
+
const typeAnn = member.typeAnnotation.typeAnnotation;
|
|
56
|
+
const allStringLiterals = typeAnn.types.every((t) => t.type === "TSLiteralType" && t.literal.type === "StringLiteral");
|
|
57
|
+
if (allStringLiterals && typeAnn.types.length >= 2) {
|
|
58
|
+
for (const [, variantNames] of cvaDefinitions) {
|
|
59
|
+
if (variantNames.includes(keyName)) {
|
|
60
|
+
context.report({
|
|
61
|
+
node: member,
|
|
62
|
+
message: `Variant prop "${keyName}" duplicates CVA variant values. ` +
|
|
63
|
+
"Use VariantProps<typeof variants> instead of manually writing union types. " +
|
|
64
|
+
"See docs/styling-guide/rules/component-variant-rule.md",
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any -- re-enable after AST node access block */
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/filename-case
|
|
3
|
+
*
|
|
4
|
+
* Enforces the naming conventions from the "Smart vs Dumb Component Rule":
|
|
5
|
+
* - Smart components: PascalCase.tsx
|
|
6
|
+
* - Dumb components: kebab-case.tsx
|
|
7
|
+
* - Non-component files: kebab-case
|
|
8
|
+
*
|
|
9
|
+
* @see docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md
|
|
10
|
+
*/
|
|
1
11
|
import path from "node:path";
|
|
2
12
|
/**
|
|
3
13
|
* Next.js App Router routing files that are exempt from filename-case checks.
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/import-boundaries
|
|
3
|
+
*
|
|
4
|
+
* Enforces the import conventions from the "Exports and Imports Rule":
|
|
5
|
+
* - Relative imports for nearby files (same folder, subfolder, one level up).
|
|
6
|
+
* - @/* alias for imports beyond one folder up.
|
|
7
|
+
* - Layer boundary enforcement (app → compositions → features → shared → root).
|
|
8
|
+
*
|
|
9
|
+
* @see docs/code-organization-guide/rules/exports-and-imports-rule.md
|
|
10
|
+
*/
|
|
1
11
|
import path from "node:path";
|
|
2
12
|
const sourceRoot = path.resolve("src");
|
|
3
13
|
const rootSupportFolders = new Set(["config", "constants", "hooks", "locales", "schemas", "types", "utils"]);
|
|
@@ -26,7 +36,7 @@ function isNearbyImport(filename, resolvedPath) {
|
|
|
26
36
|
const nonParentSegments = segments.length - parentTraversals;
|
|
27
37
|
return (parentTraversals === 0 && nonParentSegments <= 2) || (parentTraversals === 1 && nonParentSegments <= 1);
|
|
28
38
|
}
|
|
29
|
-
export const
|
|
39
|
+
export const importBoundariesRule = {
|
|
30
40
|
meta: {
|
|
31
41
|
schema: [],
|
|
32
42
|
type: "problem",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/no-arbitrary-tailwind
|
|
3
|
+
*
|
|
4
|
+
* Enforces the "Arbitrary Value Rule" — no arbitrary-value Tailwind classes.
|
|
5
|
+
*
|
|
6
|
+
* @see docs/styling-guide/rules/arbitrary-value-rule.md
|
|
7
|
+
*/
|
|
8
|
+
import type { Rule } from "eslint";
|
|
9
|
+
export declare const noArbitraryTailwindRule: Rule.RuleModule;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/no-arbitrary-tailwind
|
|
3
|
+
*
|
|
4
|
+
* Enforces the "Arbitrary Value Rule" — no arbitrary-value Tailwind classes.
|
|
5
|
+
*
|
|
6
|
+
* @see docs/styling-guide/rules/arbitrary-value-rule.md
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Matches Tailwind arbitrary-value utility classes like rounded-[13px], text-[#fff].
|
|
10
|
+
*
|
|
11
|
+
* Does NOT match variant prefixes like min-[400px]:flex-row or md:flex.
|
|
12
|
+
* The negative lookahead (?![-\w]*:) excludes anything followed by a colon + more.
|
|
13
|
+
*
|
|
14
|
+
* Note: Tailwind breakpoint variants (sm:, md:, lg:, etc.) don't use brackets,
|
|
15
|
+
* so they're never matched. Only bracket-based variants like min-[400px]: need
|
|
16
|
+
* the lookahead exclusion.
|
|
17
|
+
*/
|
|
18
|
+
const ARBITRARY_VALUE_RE = /(?:^|(?<=\s))(?:[a-z]+(?:-[a-z]+)*)-\[[^\]]+\](?![-\w]*:)/g;
|
|
19
|
+
export const noArbitraryTailwindRule = {
|
|
20
|
+
meta: {
|
|
21
|
+
schema: [],
|
|
22
|
+
type: "problem",
|
|
23
|
+
docs: {
|
|
24
|
+
description: "Disallow Tailwind arbitrary-value classes like rounded-[13px].",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
function checkQuasis(node, quasis) {
|
|
29
|
+
for (const quasi of quasis) {
|
|
30
|
+
ARBITRARY_VALUE_RE.lastIndex = 0;
|
|
31
|
+
const match = ARBITRARY_VALUE_RE.exec(quasi.value.raw);
|
|
32
|
+
if (match) {
|
|
33
|
+
context.report({
|
|
34
|
+
node,
|
|
35
|
+
message: `Tailwind arbitrary-value class "${match[0]}" is not allowed. ` +
|
|
36
|
+
"Use a named token or custom utility. " +
|
|
37
|
+
"See docs/styling-guide/rules/arbitrary-value-rule.md",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
JSXAttribute(node) {
|
|
44
|
+
if (node.name.name !== "className" && node.name.name !== "class")
|
|
45
|
+
return;
|
|
46
|
+
const valueNode = node.value;
|
|
47
|
+
if (!valueNode)
|
|
48
|
+
return;
|
|
49
|
+
if (valueNode.type === "Literal" && typeof valueNode.value === "string") {
|
|
50
|
+
ARBITRARY_VALUE_RE.lastIndex = 0;
|
|
51
|
+
const match = ARBITRARY_VALUE_RE.exec(valueNode.value);
|
|
52
|
+
if (match) {
|
|
53
|
+
context.report({
|
|
54
|
+
node,
|
|
55
|
+
message: `Tailwind arbitrary-value class "${match[0]}" is not allowed. ` +
|
|
56
|
+
"Use a named token or custom utility. " +
|
|
57
|
+
"See docs/styling-guide/rules/arbitrary-value-rule.md",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else if (valueNode.type === "JSXExpressionContainer" && valueNode.expression) {
|
|
62
|
+
const expr = valueNode.expression;
|
|
63
|
+
if (expr.type === "Literal" && typeof expr.value === "string") {
|
|
64
|
+
ARBITRARY_VALUE_RE.lastIndex = 0;
|
|
65
|
+
const match = ARBITRARY_VALUE_RE.exec(expr.value);
|
|
66
|
+
if (match) {
|
|
67
|
+
context.report({
|
|
68
|
+
node,
|
|
69
|
+
message: `Tailwind arbitrary-value class "${match[0]}" is not allowed. ` +
|
|
70
|
+
"Use a named token or custom utility. " +
|
|
71
|
+
"See docs/styling-guide/rules/arbitrary-value-rule.md",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (expr.type === "TemplateLiteral") {
|
|
76
|
+
checkQuasis(node, expr.quasis);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
CallExpression(node) {
|
|
81
|
+
if (node.callee.type !== "Identifier")
|
|
82
|
+
return;
|
|
83
|
+
const calleeName = node.callee.name;
|
|
84
|
+
if (calleeName !== "cn" && calleeName !== "clsx" && calleeName !== "twMerge" && calleeName !== "twJoin")
|
|
85
|
+
return;
|
|
86
|
+
for (const arg of node.arguments) {
|
|
87
|
+
if (arg.type === "Literal" && typeof arg.value === "string") {
|
|
88
|
+
ARBITRARY_VALUE_RE.lastIndex = 0;
|
|
89
|
+
const match = ARBITRARY_VALUE_RE.exec(arg.value);
|
|
90
|
+
if (match) {
|
|
91
|
+
context.report({
|
|
92
|
+
node,
|
|
93
|
+
message: `Tailwind arbitrary-value class "${match[0]}" is not allowed. ` +
|
|
94
|
+
"Use a named token or custom utility. " +
|
|
95
|
+
"See docs/styling-guide/rules/arbitrary-value-rule.md",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (arg.type === "TemplateLiteral") {
|
|
100
|
+
checkQuasis(node, arg.quasis);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
/* eslint-enable -- re-enable rules disabled for AST access @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument */
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/no-mixed-concerns
|
|
3
|
+
*
|
|
4
|
+
* Enforces the "No Mixed Concerns Rule" — one React component per .tsx file.
|
|
5
|
+
*
|
|
6
|
+
* @see docs/code-organization-guide/rules/no-mixed-concerns-rule.md
|
|
7
|
+
*/
|
|
8
|
+
import type { Rule } from "eslint";
|
|
9
|
+
export declare const noMixedConcernsRule: Rule.RuleModule;
|