pasika 0.3.2 → 0.3.4
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 +72 -20
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +4999 -103
- package/dist/eslint/pasika/index.d.ts +165 -53
- package/dist/eslint/pasika/index.js +4424 -89
- package/package.json +5 -6
- package/dist/cli/output.d.ts +0 -3
- package/dist/cli/output.js +0 -11
- package/dist/enforcement/coverage.d.ts +0 -51
- package/dist/enforcement/coverage.js +0 -206
- package/dist/enforcement/doctor.d.ts +0 -16
- package/dist/enforcement/doctor.js +0 -249
- package/dist/enforcement/normalize.d.ts +0 -11
- package/dist/enforcement/normalize.js +0 -21
- package/dist/enforcement/parse-docs.d.ts +0 -58
- package/dist/enforcement/parse-docs.js +0 -94
- package/dist/enforcement/types.d.ts +0 -54
- package/dist/enforcement/types.js +0 -50
- package/dist/eslint/pasika/ast-types.d.ts +0 -122
- package/dist/eslint/pasika/ast-types.js +0 -1
- package/dist/eslint/pasika/project/ccf.d.ts +0 -48
- package/dist/eslint/pasika/project/ccf.js +0 -119
- package/dist/eslint/pasika/project/index.d.ts +0 -21
- package/dist/eslint/pasika/project/index.js +0 -139
- package/dist/eslint/pasika/project/parse-module.d.ts +0 -27
- package/dist/eslint/pasika/project/parse-module.js +0 -128
- package/dist/eslint/pasika/rules/application-structure.d.ts +0 -2
- package/dist/eslint/pasika/rules/application-structure.js +0 -152
- package/dist/eslint/pasika/rules/component-conventions.d.ts +0 -16
- package/dist/eslint/pasika/rules/component-conventions.js +0 -145
- package/dist/eslint/pasika/rules/component-nesting.d.ts +0 -12
- package/dist/eslint/pasika/rules/component-nesting.js +0 -69
- package/dist/eslint/pasika/rules/component-placement.d.ts +0 -11
- package/dist/eslint/pasika/rules/component-placement.js +0 -75
- package/dist/eslint/pasika/rules/config-extraction.d.ts +0 -14
- package/dist/eslint/pasika/rules/config-extraction.js +0 -74
- package/dist/eslint/pasika/rules/cross-feature-import.d.ts +0 -2
- package/dist/eslint/pasika/rules/cross-feature-import.js +0 -75
- package/dist/eslint/pasika/rules/css/apply-usage.d.ts +0 -10
- package/dist/eslint/pasika/rules/css/apply-usage.js +0 -45
- package/dist/eslint/pasika/rules/css/base-layer-pair.d.ts +0 -10
- package/dist/eslint/pasika/rules/css/base-layer-pair.js +0 -49
- package/dist/eslint/pasika/rules/css/css-variable-naming.d.ts +0 -10
- package/dist/eslint/pasika/rules/css/css-variable-naming.js +0 -39
- package/dist/eslint/pasika/rules/css/custom-utility-apply.d.ts +0 -11
- package/dist/eslint/pasika/rules/css/custom-utility-apply.js +0 -52
- package/dist/eslint/pasika/rules/css/global-css-location.d.ts +0 -10
- package/dist/eslint/pasika/rules/css/global-css-location.js +0 -42
- package/dist/eslint/pasika/rules/css/helpers.d.ts +0 -29
- package/dist/eslint/pasika/rules/css/helpers.js +0 -93
- package/dist/eslint/pasika/rules/css/index.d.ts +0 -13
- package/dist/eslint/pasika/rules/css/index.js +0 -25
- package/dist/eslint/pasika/rules/css/root-variables.d.ts +0 -11
- package/dist/eslint/pasika/rules/css/root-variables.js +0 -53
- package/dist/eslint/pasika/rules/css/rule-tester.d.ts +0 -11
- package/dist/eslint/pasika/rules/css/rule-tester.js +0 -22
- package/dist/eslint/pasika/rules/css/stylesheet-ordering.d.ts +0 -11
- package/dist/eslint/pasika/rules/css/stylesheet-ordering.js +0 -76
- package/dist/eslint/pasika/rules/css/surface-utility.d.ts +0 -10
- package/dist/eslint/pasika/rules/css/surface-utility.js +0 -41
- package/dist/eslint/pasika/rules/css/theme-reset.d.ts +0 -9
- package/dist/eslint/pasika/rules/css/theme-reset.js +0 -36
- package/dist/eslint/pasika/rules/css/theme-variable-namespace.d.ts +0 -11
- package/dist/eslint/pasika/rules/css/theme-variable-namespace.js +0 -53
- package/dist/eslint/pasika/rules/cva-appearance-props.d.ts +0 -10
- package/dist/eslint/pasika/rules/cva-appearance-props.js +0 -77
- package/dist/eslint/pasika/rules/cva-boolean-variants.d.ts +0 -11
- package/dist/eslint/pasika/rules/cva-boolean-variants.js +0 -97
- package/dist/eslint/pasika/rules/data-testid-case.d.ts +0 -2
- package/dist/eslint/pasika/rules/data-testid-case.js +0 -76
- package/dist/eslint/pasika/rules/enforce-barrel-exports.d.ts +0 -9
- package/dist/eslint/pasika/rules/enforce-barrel-exports.js +0 -78
- package/dist/eslint/pasika/rules/enforce-cn-merge.d.ts +0 -21
- package/dist/eslint/pasika/rules/enforce-cn-merge.js +0 -86
- package/dist/eslint/pasika/rules/enforce-cva-variant-props.d.ts +0 -9
- package/dist/eslint/pasika/rules/enforce-cva-variant-props.js +0 -86
- package/dist/eslint/pasika/rules/filename-case.d.ts +0 -2
- package/dist/eslint/pasika/rules/filename-case.js +0 -123
- package/dist/eslint/pasika/rules/hook-complexity.d.ts +0 -12
- package/dist/eslint/pasika/rules/hook-complexity.js +0 -114
- package/dist/eslint/pasika/rules/hook-extraction.d.ts +0 -11
- package/dist/eslint/pasika/rules/hook-extraction.js +0 -55
- package/dist/eslint/pasika/rules/import-boundaries.d.ts +0 -2
- package/dist/eslint/pasika/rules/import-boundaries.js +0 -167
- package/dist/eslint/pasika/rules/import-through-index.d.ts +0 -2
- package/dist/eslint/pasika/rules/import-through-index.js +0 -52
- package/dist/eslint/pasika/rules/interactive-component.d.ts +0 -10
- package/dist/eslint/pasika/rules/interactive-component.js +0 -76
- package/dist/eslint/pasika/rules/json/index.d.ts +0 -5
- package/dist/eslint/pasika/rules/json/index.js +0 -9
- package/dist/eslint/pasika/rules/json/no-cache-flag.d.ts +0 -10
- package/dist/eslint/pasika/rules/json/no-cache-flag.js +0 -45
- package/dist/eslint/pasika/rules/json/no-vulyk-dependency.d.ts +0 -10
- package/dist/eslint/pasika/rules/json/no-vulyk-dependency.js +0 -41
- package/dist/eslint/pasika/rules/json/rule-tester.d.ts +0 -11
- package/dist/eslint/pasika/rules/json/rule-tester.js +0 -21
- package/dist/eslint/pasika/rules/jsx-hygiene.d.ts +0 -9
- package/dist/eslint/pasika/rules/jsx-hygiene.js +0 -108
- package/dist/eslint/pasika/rules/locale-dotted-path.d.ts +0 -10
- package/dist/eslint/pasika/rules/locale-dotted-path.js +0 -65
- package/dist/eslint/pasika/rules/locale-placement.d.ts +0 -13
- package/dist/eslint/pasika/rules/locale-placement.js +0 -163
- package/dist/eslint/pasika/rules/locales-location.d.ts +0 -11
- package/dist/eslint/pasika/rules/locales-location.js +0 -56
- package/dist/eslint/pasika/rules/md/doc-kind-suffix.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/doc-kind-suffix.js +0 -29
- package/dist/eslint/pasika/rules/md/example-heading-description.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/example-heading-description.js +0 -30
- package/dist/eslint/pasika/rules/md/glossary-term-linking.d.ts +0 -2
- package/dist/eslint/pasika/rules/md/glossary-term-linking.js +0 -99
- package/dist/eslint/pasika/rules/md/guide-folder-entry-point.d.ts +0 -2
- package/dist/eslint/pasika/rules/md/guide-folder-entry-point.js +0 -46
- package/dist/eslint/pasika/rules/md/guide-link-anchors.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/guide-link-anchors.js +0 -50
- package/dist/eslint/pasika/rules/md/guide-overview-no-links.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/guide-overview-no-links.js +0 -51
- package/dist/eslint/pasika/rules/md/guide-states-no-requirement.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/guide-states-no-requirement.js +0 -26
- package/dist/eslint/pasika/rules/md/guide-step-single-link.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/guide-step-single-link.js +0 -48
- package/dist/eslint/pasika/rules/md/guide-step-single-sentence.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/guide-step-single-sentence.js +0 -42
- package/dist/eslint/pasika/rules/md/helpers.d.ts +0 -25
- package/dist/eslint/pasika/rules/md/helpers.js +0 -37
- package/dist/eslint/pasika/rules/md/index.d.ts +0 -27
- package/dist/eslint/pasika/rules/md/index.js +0 -53
- package/dist/eslint/pasika/rules/md/no-cross-document-link.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/no-cross-document-link.js +0 -36
- package/dist/eslint/pasika/rules/md/no-nested-how-to.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/no-nested-how-to.js +0 -39
- package/dist/eslint/pasika/rules/md/no-template-prompt.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/no-template-prompt.js +0 -22
- package/dist/eslint/pasika/rules/md/overview-length.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/overview-length.js +0 -47
- package/dist/eslint/pasika/rules/md/overview-present.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/overview-present.js +0 -41
- package/dist/eslint/pasika/rules/md/policy-no-examples.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/policy-no-examples.js +0 -28
- package/dist/eslint/pasika/rules/md/policy-single-document.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/policy-single-document.js +0 -34
- package/dist/eslint/pasika/rules/md/policy-subject-headings.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/policy-subject-headings.js +0 -58
- package/dist/eslint/pasika/rules/md/project-index.d.ts +0 -17
- package/dist/eslint/pasika/rules/md/project-index.js +0 -67
- package/dist/eslint/pasika/rules/md/reference-block-headings.d.ts +0 -6
- package/dist/eslint/pasika/rules/md/reference-block-headings.js +0 -31
- package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.d.ts +0 -6
- package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.js +0 -26
- package/dist/eslint/pasika/rules/md/requirement-present.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/requirement-present.js +0 -41
- package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.js +0 -46
- package/dist/eslint/pasika/rules/md/rule-paired-examples.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/rule-paired-examples.js +0 -36
- package/dist/eslint/pasika/rules/md/rule-tester.d.ts +0 -11
- package/dist/eslint/pasika/rules/md/rule-tester.js +0 -21
- package/dist/eslint/pasika/rules/md/support-document-placement.d.ts +0 -2
- package/dist/eslint/pasika/rules/md/support-document-placement.js +0 -36
- package/dist/eslint/pasika/rules/md/title-matches-file-name.d.ts +0 -2
- package/dist/eslint/pasika/rules/md/title-matches-file-name.js +0 -46
- package/dist/eslint/pasika/rules/named-exports.d.ts +0 -2
- package/dist/eslint/pasika/rules/named-exports.js +0 -49
- package/dist/eslint/pasika/rules/no-arbitrary-tailwind.d.ts +0 -9
- package/dist/eslint/pasika/rules/no-arbitrary-tailwind.js +0 -97
- package/dist/eslint/pasika/rules/no-eslint-disable.d.ts +0 -10
- package/dist/eslint/pasika/rules/no-eslint-disable.js +0 -33
- package/dist/eslint/pasika/rules/no-mixed-concerns.d.ts +0 -9
- package/dist/eslint/pasika/rules/no-mixed-concerns.js +0 -71
- package/dist/eslint/pasika/rules/no-util-barrel.d.ts +0 -2
- package/dist/eslint/pasika/rules/no-util-barrel.js +0 -48
- package/dist/eslint/pasika/rules/pure-function-extract.d.ts +0 -9
- package/dist/eslint/pasika/rules/pure-function-extract.js +0 -92
- package/dist/eslint/pasika/rules/shared-style-dedup.d.ts +0 -13
- package/dist/eslint/pasika/rules/shared-style-dedup.js +0 -91
- package/dist/eslint/pasika/rules/stay-flat.d.ts +0 -12
- package/dist/eslint/pasika/rules/stay-flat.js +0 -81
- package/dist/eslint/pasika/rules/support-file-placement.d.ts +0 -15
- package/dist/eslint/pasika/rules/support-file-placement.js +0 -70
- package/dist/eslint/pasika/rules/support-folder-shape.d.ts +0 -2
- package/dist/eslint/pasika/rules/support-folder-shape.js +0 -59
- package/dist/eslint/pasika/rules/type-extraction.d.ts +0 -13
- package/dist/eslint/pasika/rules/type-extraction.js +0 -85
- package/dist/eslint/pasika/rules/ui-state.d.ts +0 -9
- package/dist/eslint/pasika/rules/ui-state.js +0 -99
- package/dist/eslint/pasika/rules/util-file-name.d.ts +0 -2
- package/dist/eslint/pasika/rules/util-file-name.js +0 -48
- package/dist/eslint/pasika/rules/value-extraction.d.ts +0 -13
- package/dist/eslint/pasika/rules/value-extraction.js +0 -48
- package/enforcement/registry.json +0 -1145
|
@@ -1,71 +0,0 @@
|
|
|
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
|
-
function getExportName(declaration) {
|
|
9
|
-
if (declaration?.type === "FunctionDeclaration" && declaration.id) {
|
|
10
|
-
return declaration.id.name ?? null;
|
|
11
|
-
}
|
|
12
|
-
if (declaration?.type === "VariableDeclaration") {
|
|
13
|
-
const declarator = declaration.declarations?.[0];
|
|
14
|
-
if (declarator?.id?.type === "Identifier" &&
|
|
15
|
-
(declarator.init?.type === "ArrowFunctionExpression" || declarator.init?.type === "FunctionExpression")) {
|
|
16
|
-
return declarator.id.name ?? null;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
if (declaration?.type === "ArrowFunctionExpression") {
|
|
20
|
-
return "default";
|
|
21
|
-
}
|
|
22
|
-
if (declaration?.type === "FunctionExpression" && declaration.id) {
|
|
23
|
-
return declaration.id.name ?? null;
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
export const noMixedConcernsRule = {
|
|
28
|
-
meta: {
|
|
29
|
-
schema: [],
|
|
30
|
-
type: "problem",
|
|
31
|
-
docs: {
|
|
32
|
-
description: "Enforce one exported React component per .tsx file.",
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
create(context) {
|
|
36
|
-
if (!context.filename.endsWith(".tsx"))
|
|
37
|
-
return {};
|
|
38
|
-
let exportedComponentCount = 0;
|
|
39
|
-
const extraExports = [];
|
|
40
|
-
function registerExport(name) {
|
|
41
|
-
exportedComponentCount++;
|
|
42
|
-
if (exportedComponentCount > 1) {
|
|
43
|
-
extraExports.push({ name });
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return {
|
|
47
|
-
ExportNamedDeclaration(node) {
|
|
48
|
-
const name = getExportName(node.declaration);
|
|
49
|
-
if (name)
|
|
50
|
-
registerExport(name);
|
|
51
|
-
},
|
|
52
|
-
ExportDefaultDeclaration(node) {
|
|
53
|
-
const name = getExportName(node.declaration);
|
|
54
|
-
if (name)
|
|
55
|
-
registerExport(name);
|
|
56
|
-
},
|
|
57
|
-
"Program:exit"() {
|
|
58
|
-
if (exportedComponentCount > 1) {
|
|
59
|
-
for (const extra of extraExports) {
|
|
60
|
-
context.report({
|
|
61
|
-
loc: { line: 1, column: 0 },
|
|
62
|
-
message: `File exports multiple components. "${extra.name}" is an extra component export. ` +
|
|
63
|
-
"Move it to its own file. Each .tsx file MUST contain exactly one component. " +
|
|
64
|
-
"See docs/code-organization-guide/rules/no-mixed-concerns-rule.md",
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
},
|
|
71
|
-
};
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { resolveSpecifier } from "../project/index.js";
|
|
3
|
-
export const noUtilBarrelRule = {
|
|
4
|
-
meta: {
|
|
5
|
-
schema: [],
|
|
6
|
-
type: "problem",
|
|
7
|
-
docs: {
|
|
8
|
-
description: "Require utilities to be imported directly without a barrel.",
|
|
9
|
-
},
|
|
10
|
-
},
|
|
11
|
-
create(context) {
|
|
12
|
-
const filename = path.resolve(context.filename);
|
|
13
|
-
const sourceRoot = sourceRootOf(filename);
|
|
14
|
-
return {
|
|
15
|
-
Program(node) {
|
|
16
|
-
for (const specifier of importSpecifiers(context.sourceCode.text)) {
|
|
17
|
-
const target = resolveSpecifier(filename, specifier, sourceRoot);
|
|
18
|
-
if (!target)
|
|
19
|
-
continue;
|
|
20
|
-
const segments = target.replace(/\\/g, "/").split("/");
|
|
21
|
-
const utilsIndex = segments.lastIndexOf("utils");
|
|
22
|
-
if (utilsIndex < 0 || !path.basename(target).startsWith("index."))
|
|
23
|
-
continue;
|
|
24
|
-
context.report({
|
|
25
|
-
node,
|
|
26
|
-
message: `Import utilities directly instead of through "${specifier}". ` +
|
|
27
|
-
"See docs/code-organization-guide/rules/utilities-rule.md",
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
function importSpecifiers(source) {
|
|
35
|
-
const specifiers = [];
|
|
36
|
-
const pattern = /\bimport\s+(?:(?:type\s+)?[\s\S]*?\sfrom\s+)?["'](?<specifier>[^"']+)["']/g;
|
|
37
|
-
for (const match of source.matchAll(pattern)) {
|
|
38
|
-
const specifier = match.groups?.specifier;
|
|
39
|
-
if (specifier)
|
|
40
|
-
specifiers.push(specifier);
|
|
41
|
-
}
|
|
42
|
-
return specifiers;
|
|
43
|
-
}
|
|
44
|
-
function sourceRootOf(filename) {
|
|
45
|
-
const marker = `${path.sep}src${path.sep}`;
|
|
46
|
-
const srcIndex = filename.lastIndexOf(marker);
|
|
47
|
-
return srcIndex >= 0 ? filename.slice(0, srcIndex + marker.length - 1) : path.resolve("src");
|
|
48
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint rule: pasika/pure-function-extract
|
|
3
|
-
*
|
|
4
|
-
* A pure function MUST be extracted to utils/, even when it has one consumer.
|
|
5
|
-
*
|
|
6
|
-
* @see docs/code-organization-guide/rules/utilities-rule.md
|
|
7
|
-
*/
|
|
8
|
-
import type { Rule } from "eslint";
|
|
9
|
-
export declare const pureFunctionExtractRule: Rule.RuleModule;
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint rule: pasika/pure-function-extract
|
|
3
|
-
*
|
|
4
|
-
* A pure function MUST be extracted to utils/, even when it has one consumer.
|
|
5
|
-
*
|
|
6
|
-
* @see docs/code-organization-guide/rules/utilities-rule.md
|
|
7
|
-
*/
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
function isComponentLikeName(name) {
|
|
10
|
-
return /^[A-Z]/.test(name);
|
|
11
|
-
}
|
|
12
|
-
function isHookName(name) {
|
|
13
|
-
return /^use[A-Z]/.test(name);
|
|
14
|
-
}
|
|
15
|
-
function hasHookUsage(body) {
|
|
16
|
-
for (const stmt of body.body) {
|
|
17
|
-
if (stmt.type !== "ExpressionStatement")
|
|
18
|
-
continue;
|
|
19
|
-
const expr = stmt.expression;
|
|
20
|
-
if (expr.type === "CallExpression" && expr.callee.type === "Identifier" && isHookName(expr.callee.name)) {
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
export const pureFunctionExtractRule = {
|
|
27
|
-
meta: {
|
|
28
|
-
schema: [],
|
|
29
|
-
type: "problem",
|
|
30
|
-
docs: {
|
|
31
|
-
description: "Require pure functions declared in component files to be extracted to utils/.",
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
create(context) {
|
|
35
|
-
const filename = context.filename;
|
|
36
|
-
if (!filename.endsWith(".tsx") && !filename.endsWith(".jsx"))
|
|
37
|
-
return {};
|
|
38
|
-
const sourceRoot = path.resolve("src");
|
|
39
|
-
const relative = path.relative(sourceRoot, filename);
|
|
40
|
-
if (relative.startsWith(".."))
|
|
41
|
-
return {};
|
|
42
|
-
const segments = relative.split(path.sep);
|
|
43
|
-
// Already in utils/ or a support folder — fine
|
|
44
|
-
if (segments[0] === "utils")
|
|
45
|
-
return {};
|
|
46
|
-
if (segments[0] === "app")
|
|
47
|
-
return {};
|
|
48
|
-
const supportFolders = new Set(["hooks", "types", "schemas", "constants", "utils"]);
|
|
49
|
-
if (segments.length >= 2 && supportFolders.has(segments[segments.length - 1] ?? ""))
|
|
50
|
-
return {};
|
|
51
|
-
function report(node, name) {
|
|
52
|
-
context.report({
|
|
53
|
-
node,
|
|
54
|
-
message: `Extract pure function "${name}" to utils/. See docs/code-organization-guide/rules/utilities-rule.md`,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return {
|
|
58
|
-
FunctionDeclaration(node) {
|
|
59
|
-
const exported = node.parent?.type === "ExportNamedDeclaration";
|
|
60
|
-
if (!exported)
|
|
61
|
-
return;
|
|
62
|
-
const name = node.id?.name;
|
|
63
|
-
if (!name)
|
|
64
|
-
return;
|
|
65
|
-
if (isComponentLikeName(name) || isHookName(name))
|
|
66
|
-
return;
|
|
67
|
-
if (!node.body || hasHookUsage(node.body))
|
|
68
|
-
return;
|
|
69
|
-
report(node, name);
|
|
70
|
-
},
|
|
71
|
-
VariableDeclarator(node) {
|
|
72
|
-
if (node.id.type !== "Identifier")
|
|
73
|
-
return;
|
|
74
|
-
const name = node.id.name;
|
|
75
|
-
if (!name)
|
|
76
|
-
return;
|
|
77
|
-
const exported = node.parent.parent?.type === "ExportNamedDeclaration";
|
|
78
|
-
if (!exported)
|
|
79
|
-
return;
|
|
80
|
-
if (isComponentLikeName(name) || isHookName(name))
|
|
81
|
-
return;
|
|
82
|
-
const init = node.init;
|
|
83
|
-
if (!init || (init.type !== "ArrowFunctionExpression" && init.type !== "FunctionExpression")) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
if (init.body.type === "BlockStatement" && hasHookUsage(init.body))
|
|
87
|
-
return;
|
|
88
|
-
report(node, name);
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
},
|
|
92
|
-
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint rule: pasika/shared-style-dedup
|
|
3
|
-
*
|
|
4
|
-
* A style or group of styles that two or more components use together and
|
|
5
|
-
* should change together MUST become one named custom utility. Styling used by
|
|
6
|
-
* only one component MUST stay in that component file as static Tailwind
|
|
7
|
-
* utility classes. The rule compares className lists across the whole tree,
|
|
8
|
-
* because a single file cannot see what other components use.
|
|
9
|
-
*
|
|
10
|
-
* @see docs/styling-guide/rules/theme-and-utility-definition-rule.md
|
|
11
|
-
*/
|
|
12
|
-
import type { Rule } from "eslint";
|
|
13
|
-
export declare const sharedStyleDedupRule: Rule.RuleModule;
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint rule: pasika/shared-style-dedup
|
|
3
|
-
*
|
|
4
|
-
* A style or group of styles that two or more components use together and
|
|
5
|
-
* should change together MUST become one named custom utility. Styling used by
|
|
6
|
-
* only one component MUST stay in that component file as static Tailwind
|
|
7
|
-
* utility classes. The rule compares className lists across the whole tree,
|
|
8
|
-
* because a single file cannot see what other components use.
|
|
9
|
-
*
|
|
10
|
-
* @see docs/styling-guide/rules/theme-and-utility-definition-rule.md
|
|
11
|
-
*/
|
|
12
|
-
import path from "node:path";
|
|
13
|
-
import { readFileSync, statSync } from "node:fs";
|
|
14
|
-
import { getProjectIndex } from "../project/index.js";
|
|
15
|
-
import { segmentsOf } from "../project/ccf.js";
|
|
16
|
-
/** Static className string literals; dynamic class lists are not comparable across files. */
|
|
17
|
-
const CLASS_NAME = /className="(?<classes>[^"]+)"/g;
|
|
18
|
-
let comboCache;
|
|
19
|
-
/** className combinations used by each component file, memoized per tree. */
|
|
20
|
-
function combosFor(index) {
|
|
21
|
-
const files = [...index.modules.keys()]
|
|
22
|
-
.filter((file) => file.endsWith(".tsx") || file.endsWith(".jsx"))
|
|
23
|
-
.sort((left, right) => left.localeCompare(right));
|
|
24
|
-
let fingerprint = "";
|
|
25
|
-
for (const file of files) {
|
|
26
|
-
try {
|
|
27
|
-
fingerprint += String(statSync(file).mtimeMs);
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
fingerprint += "0";
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
if (comboCache?.sourceRoot === index.sourceRoot && comboCache.fingerprint === fingerprint) {
|
|
34
|
-
return comboCache.combos;
|
|
35
|
-
}
|
|
36
|
-
const combos = new Map();
|
|
37
|
-
for (const file of files) {
|
|
38
|
-
const text = readFileSync(file, "utf8");
|
|
39
|
-
for (const match of text.matchAll(CLASS_NAME)) {
|
|
40
|
-
const classes = (match.groups?.classes ?? "").split(/\s+/).filter(Boolean);
|
|
41
|
-
if (classes.length < 2)
|
|
42
|
-
continue;
|
|
43
|
-
const key = [...classes].sort().join(" ");
|
|
44
|
-
const users = combos.get(key) ?? new Set();
|
|
45
|
-
users.add(file);
|
|
46
|
-
combos.set(key, users);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
comboCache = { sourceRoot: index.sourceRoot, fingerprint, combos };
|
|
50
|
-
return combos;
|
|
51
|
-
}
|
|
52
|
-
export const sharedStyleDedupRule = {
|
|
53
|
-
meta: {
|
|
54
|
-
schema: [],
|
|
55
|
-
type: "problem",
|
|
56
|
-
docs: {
|
|
57
|
-
description: "Require a className combination used by two or more components to become a named custom utility.",
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
create(context) {
|
|
61
|
-
const sourceRoot = path.resolve("src");
|
|
62
|
-
const file = path.resolve(context.filename);
|
|
63
|
-
const segments = segmentsOf(file, sourceRoot);
|
|
64
|
-
if (segments.length === 0)
|
|
65
|
-
return {};
|
|
66
|
-
const index = getProjectIndex(sourceRoot);
|
|
67
|
-
if (!index)
|
|
68
|
-
return {};
|
|
69
|
-
// Report each shared combination once, on the alphabetically first file
|
|
70
|
-
// that uses it, so the whole tree produces one finding per combination.
|
|
71
|
-
const combos = combosFor(index);
|
|
72
|
-
const sharedHere = [...combos.entries()]
|
|
73
|
-
.filter(([, users]) => users.size >= 2 && users.has(file))
|
|
74
|
-
.filter(([, users]) => [...users].sort((left, right) => left.localeCompare(right))[0] === file);
|
|
75
|
-
if (sharedHere.length === 0)
|
|
76
|
-
return {};
|
|
77
|
-
return {
|
|
78
|
-
Program(node) {
|
|
79
|
-
for (const [combo, users] of sharedHere) {
|
|
80
|
-
context.report({
|
|
81
|
-
node,
|
|
82
|
-
loc: { line: 1, column: 0 },
|
|
83
|
-
message: `Class combination "${combo}" is used by ${String(users.size)} components and should change ` +
|
|
84
|
-
"together; create a named custom Tailwind utility for it. " +
|
|
85
|
-
"See docs/styling-guide/rules/theme-and-utility-definition-rule.md",
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
},
|
|
91
|
-
};
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint rule: pasika/stay-flat
|
|
3
|
-
*
|
|
4
|
-
* A component MUST stay flat until it has one or more exclusive child
|
|
5
|
-
* components, then MUST be nested in a folder with the same name. An exclusive
|
|
6
|
-
* child is a sibling component that only this component uses, so a flat
|
|
7
|
-
* component that owns such children must move into a folder named after it.
|
|
8
|
-
*
|
|
9
|
-
* @see docs/code-organization-guide/rules/folder-nesting-rule.md
|
|
10
|
-
*/
|
|
11
|
-
import type { Rule } from "eslint";
|
|
12
|
-
export declare const stayFlatRule: Rule.RuleModule;
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint rule: pasika/stay-flat
|
|
3
|
-
*
|
|
4
|
-
* A component MUST stay flat until it has one or more exclusive child
|
|
5
|
-
* components, then MUST be nested in a folder with the same name. An exclusive
|
|
6
|
-
* child is a sibling component that only this component uses, so a flat
|
|
7
|
-
* component that owns such children must move into a folder named after it.
|
|
8
|
-
*
|
|
9
|
-
* @see docs/code-organization-guide/rules/folder-nesting-rule.md
|
|
10
|
-
*/
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
import { getProjectIndex } from "../project/index.js";
|
|
13
|
-
import { segmentsOf } from "../project/ccf.js";
|
|
14
|
-
export const stayFlatRule = {
|
|
15
|
-
meta: {
|
|
16
|
-
schema: [],
|
|
17
|
-
type: "problem",
|
|
18
|
-
docs: {
|
|
19
|
-
description: "Require a component with exclusive children to be nested in a folder with the same name.",
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
create(context) {
|
|
23
|
-
const sourceRoot = path.resolve("src");
|
|
24
|
-
const file = path.resolve(context.filename);
|
|
25
|
-
const segments = segmentsOf(file, sourceRoot);
|
|
26
|
-
// A component file directly inside a feature folder: src/features/<feature>/<file>
|
|
27
|
-
if (segments.length !== 3 || segments[0] !== "features")
|
|
28
|
-
return {};
|
|
29
|
-
const index = getProjectIndex(sourceRoot);
|
|
30
|
-
if (!index)
|
|
31
|
-
return {};
|
|
32
|
-
const module = index.modules.get(file);
|
|
33
|
-
if (!module?.exports.some((exp) => exp.kind === "component"))
|
|
34
|
-
return {};
|
|
35
|
-
const featureName = segments[1];
|
|
36
|
-
if (!featureName)
|
|
37
|
-
return {};
|
|
38
|
-
// A sibling component file is an exclusive child when every file that
|
|
39
|
-
// imports it is this component or a support file in the same feature folder.
|
|
40
|
-
const exclusiveChildren = [...index.modules.values()].filter((candidate) => {
|
|
41
|
-
if (candidate.file === file)
|
|
42
|
-
return false;
|
|
43
|
-
const candidateSegments = segmentsOf(candidate.file, sourceRoot);
|
|
44
|
-
if (candidateSegments.length !== 3)
|
|
45
|
-
return false;
|
|
46
|
-
if (candidateSegments[0] !== "features" || candidateSegments[1] !== featureName)
|
|
47
|
-
return false;
|
|
48
|
-
if (candidateSegments[2]?.startsWith("index."))
|
|
49
|
-
return false;
|
|
50
|
-
if (!candidate.exports.some((exp) => exp.kind === "component"))
|
|
51
|
-
return false;
|
|
52
|
-
const consumers = index.consumers.get(candidate.file) ?? new Set();
|
|
53
|
-
if (consumers.size === 0)
|
|
54
|
-
return false;
|
|
55
|
-
return [...consumers].every((consumer) => {
|
|
56
|
-
if (consumer === file)
|
|
57
|
-
return true;
|
|
58
|
-
const consumerSegments = segmentsOf(consumer, sourceRoot);
|
|
59
|
-
if (consumerSegments[0] !== "features" || consumerSegments[1] !== featureName)
|
|
60
|
-
return false;
|
|
61
|
-
return !(index.modules.get(consumer)?.exports.some((exp) => exp.kind === "component") ?? false);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
if (exclusiveChildren.length === 0)
|
|
65
|
-
return {};
|
|
66
|
-
const childNames = exclusiveChildren
|
|
67
|
-
.map((child) => child.exports.find((exp) => exp.kind === "component")?.name)
|
|
68
|
-
.filter((name) => name !== undefined);
|
|
69
|
-
return {
|
|
70
|
-
Program(node) {
|
|
71
|
-
context.report({
|
|
72
|
-
node,
|
|
73
|
-
loc: { line: 1, column: 0 },
|
|
74
|
-
message: `This component has exclusive child component(s) ${childNames.join(", ")}; ` +
|
|
75
|
-
`nest it in a folder named after it inside src/features/${featureName}/. ` +
|
|
76
|
-
"See docs/code-organization-guide/rules/folder-nesting-rule.md",
|
|
77
|
-
});
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
},
|
|
81
|
-
};
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint rule: pasika/support-file-placement
|
|
3
|
-
*
|
|
4
|
-
* Enforces where a hook, type, schema, constant, or utility lives, which depends
|
|
5
|
-
* on the files that import it and so cannot be decided from one file.
|
|
6
|
-
*
|
|
7
|
-
* @see docs/code-organization-guide/rules/types-and-schemas-rule.md
|
|
8
|
-
* @see docs/code-organization-guide/rules/constants-rule.md
|
|
9
|
-
* @see docs/code-organization-guide/rules/utilities-rule.md
|
|
10
|
-
* @see docs/code-organization-guide/rules/hook-extraction-rule.md
|
|
11
|
-
* @see docs/code-organization-guide/rules/configuration-rule.md
|
|
12
|
-
* @see docs/code-organization-guide/rules/folder-nesting-rule.md
|
|
13
|
-
*/
|
|
14
|
-
import type { Rule } from "eslint";
|
|
15
|
-
export declare const supportFilePlacementRule: Rule.RuleModule;
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint rule: pasika/support-file-placement
|
|
3
|
-
*
|
|
4
|
-
* Enforces where a hook, type, schema, constant, or utility lives, which depends
|
|
5
|
-
* on the files that import it and so cannot be decided from one file.
|
|
6
|
-
*
|
|
7
|
-
* @see docs/code-organization-guide/rules/types-and-schemas-rule.md
|
|
8
|
-
* @see docs/code-organization-guide/rules/constants-rule.md
|
|
9
|
-
* @see docs/code-organization-guide/rules/utilities-rule.md
|
|
10
|
-
* @see docs/code-organization-guide/rules/hook-extraction-rule.md
|
|
11
|
-
* @see docs/code-organization-guide/rules/configuration-rule.md
|
|
12
|
-
* @see docs/code-organization-guide/rules/folder-nesting-rule.md
|
|
13
|
-
*/
|
|
14
|
-
import path from "node:path";
|
|
15
|
-
import { getProjectIndex } from "../project/index.js";
|
|
16
|
-
import { describeConsumers, folderSegmentsOf, formatFolder, isConfigModule, resolveSupportPlacement, segmentsOf, SUPPORT_FOLDERS, } from "../project/ccf.js";
|
|
17
|
-
/**
|
|
18
|
-
* Support folders a configuration module keeps regardless of who imports them: a
|
|
19
|
-
* constant or type whose meaning is derived from the configuration stays beside
|
|
20
|
-
* it even when consumers exist outside the module.
|
|
21
|
-
*/
|
|
22
|
-
const CONFIG_OWNED_FOLDERS = new Set(["types", "constants"]);
|
|
23
|
-
const REASON_TEXT = {
|
|
24
|
-
"app-consumer": "a file under src/app/ imports it, so it belongs to the app-wide support folder",
|
|
25
|
-
"config-module": "every file that imports it belongs to that configuration module",
|
|
26
|
-
ccf: "that is the closest folder its consumers share",
|
|
27
|
-
"across-features": "its consumers span more than one feature, so no feature can own it",
|
|
28
|
-
"across-layers": "its consumers span more than one layer, so no layer can own it",
|
|
29
|
-
};
|
|
30
|
-
const sameFolder = (left, right) => left.length === right.length && left.every((segment, depth) => segment === right[depth]);
|
|
31
|
-
export const supportFilePlacementRule = {
|
|
32
|
-
meta: {
|
|
33
|
-
schema: [],
|
|
34
|
-
type: "problem",
|
|
35
|
-
docs: {
|
|
36
|
-
description: "Enforce that a support file lives in the folder its consumers imply.",
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
create(context) {
|
|
40
|
-
const sourceRoot = path.resolve("src");
|
|
41
|
-
const supportFile = path.resolve(context.filename);
|
|
42
|
-
const currentFolder = folderSegmentsOf(supportFile, sourceRoot);
|
|
43
|
-
const supportFolder = currentFolder[currentFolder.length - 1];
|
|
44
|
-
// Only a file already inside a support folder is placed by this rule; a
|
|
45
|
-
// declaration still sitting beside its consumer is an extraction question.
|
|
46
|
-
if (supportFolder === undefined || !SUPPORT_FOLDERS.has(supportFolder))
|
|
47
|
-
return {};
|
|
48
|
-
// A type or constant whose meaning comes from a configuration may stay with
|
|
49
|
-
// it however widely it is used, so its consumers cannot place it.
|
|
50
|
-
if (isConfigModule(segmentsOf(supportFile, sourceRoot)) && CONFIG_OWNED_FOLDERS.has(supportFolder))
|
|
51
|
-
return {};
|
|
52
|
-
const index = getProjectIndex(sourceRoot);
|
|
53
|
-
if (!index)
|
|
54
|
-
return {};
|
|
55
|
-
const placement = resolveSupportPlacement(supportFile, supportFolder, index);
|
|
56
|
-
if (!placement || sameFolder(currentFolder, placement.expectedFolder))
|
|
57
|
-
return {};
|
|
58
|
-
return {
|
|
59
|
-
Program(node) {
|
|
60
|
-
context.report({
|
|
61
|
-
node,
|
|
62
|
-
loc: { line: 1, column: 0 },
|
|
63
|
-
message: `Move this file to ${formatFolder(placement.expectedFolder)} — ` +
|
|
64
|
-
`${REASON_TEXT[placement.reason] ?? "that is where its consumers place it"}. ` +
|
|
65
|
-
`Imported by ${describeConsumers(placement.countedConsumers, sourceRoot)}.`,
|
|
66
|
-
});
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
},
|
|
70
|
-
};
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
const SUPPORT_FOLDERS = new Set(["constants", "types", "schemas"]);
|
|
4
|
-
const INDEX_NAMES = new Set(["index.ts", "index.tsx", "index.mts", "index.cts"]);
|
|
5
|
-
export const supportFolderShapeRule = {
|
|
6
|
-
meta: {
|
|
7
|
-
schema: [],
|
|
8
|
-
type: "problem",
|
|
9
|
-
docs: {
|
|
10
|
-
description: "Require support-folder exports to be defined in index.ts or named-re-exported by it.",
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
create(context) {
|
|
14
|
-
const filename = path.resolve(context.filename);
|
|
15
|
-
const baseName = path.basename(filename);
|
|
16
|
-
if (!INDEX_NAMES.has(baseName))
|
|
17
|
-
return {};
|
|
18
|
-
const folder = path.basename(path.dirname(filename));
|
|
19
|
-
if (!SUPPORT_FOLDERS.has(folder))
|
|
20
|
-
return {};
|
|
21
|
-
const directory = path.dirname(filename);
|
|
22
|
-
let entries;
|
|
23
|
-
try {
|
|
24
|
-
entries = fs.readdirSync(directory);
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return {};
|
|
28
|
-
}
|
|
29
|
-
const siblingModules = entries.filter((entry) => entry !== baseName && /\.(?:[cm]?tsx?|jsx?)$/.test(entry));
|
|
30
|
-
if (siblingModules.length === 0)
|
|
31
|
-
return {};
|
|
32
|
-
return {
|
|
33
|
-
Program(node) {
|
|
34
|
-
const source = context.sourceCode.text;
|
|
35
|
-
const hasDirectExport = /export\s+(?:const|let|var|function|class|type|interface|enum)\b/.test(source);
|
|
36
|
-
if (hasDirectExport)
|
|
37
|
-
return;
|
|
38
|
-
const exportedFiles = new Set();
|
|
39
|
-
const exportPattern = /export\s+(?:\{[^}]*\}|\*[^;]*)\s+from\s+["'](?<specifier>\.[^"']+)["']/g;
|
|
40
|
-
for (const match of source.matchAll(exportPattern)) {
|
|
41
|
-
const specifier = match.groups?.specifier;
|
|
42
|
-
if (specifier)
|
|
43
|
-
exportedFiles.add(path.basename(specifier));
|
|
44
|
-
}
|
|
45
|
-
const missing = siblingModules.filter((entry) => {
|
|
46
|
-
const stem = entry.replace(/\.(?:[cm]?tsx?|jsx?)$/, "");
|
|
47
|
-
return !exportedFiles.has(stem);
|
|
48
|
-
});
|
|
49
|
-
if (missing.length === 0)
|
|
50
|
-
return;
|
|
51
|
-
context.report({
|
|
52
|
-
node,
|
|
53
|
-
message: `${folder}/index.ts must named-re-export every support file: ${missing.join(", ")}. ` +
|
|
54
|
-
`See docs/code-organization-guide/rules/${folder === "constants" ? "constants" : "types-and-schemas"}-rule.md`,
|
|
55
|
-
});
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
},
|
|
59
|
-
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint rule: pasika/type-extraction
|
|
3
|
-
*
|
|
4
|
-
* A type or schema declared in a component MUST stay in that component file
|
|
5
|
-
* until another file imports it without the component where it is defined, and
|
|
6
|
-
* a type or schema declared outside a component MUST stay in its file until
|
|
7
|
-
* another file needs it without using the code in that file. Importing the
|
|
8
|
-
* type alongside the component that defines it never requires extraction.
|
|
9
|
-
*
|
|
10
|
-
* @see docs/code-organization-guide/rules/types-and-schemas-rule.md
|
|
11
|
-
*/
|
|
12
|
-
import type { Rule } from "eslint";
|
|
13
|
-
export declare const typeExtractionRule: Rule.RuleModule;
|