pasika 0.2.0 → 0.3.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.
Files changed (30) hide show
  1. package/README.md +93 -57
  2. package/dist/cli/index.d.ts +2 -0
  3. package/dist/cli/index.js +132 -0
  4. package/dist/enforcement/coverage.d.ts +51 -0
  5. package/dist/enforcement/coverage.js +210 -0
  6. package/dist/enforcement/docs-check.d.ts +17 -0
  7. package/dist/enforcement/docs-check.js +159 -0
  8. package/dist/enforcement/normalize.d.ts +11 -0
  9. package/dist/enforcement/normalize.js +21 -0
  10. package/dist/enforcement/parse-docs.d.ts +58 -0
  11. package/dist/enforcement/parse-docs.js +94 -0
  12. package/dist/enforcement/types.d.ts +57 -0
  13. package/dist/enforcement/types.js +59 -0
  14. package/dist/eslint/pasika/index.d.ts +9 -15
  15. package/dist/eslint/pasika/index.js +18 -20
  16. package/dist/eslint/pasika/project/ccf.d.ts +48 -0
  17. package/dist/eslint/pasika/project/ccf.js +119 -0
  18. package/dist/eslint/pasika/project/index.d.ts +21 -0
  19. package/dist/eslint/pasika/project/index.js +139 -0
  20. package/dist/eslint/pasika/project/parse-module.d.ts +27 -0
  21. package/dist/eslint/pasika/project/parse-module.js +128 -0
  22. package/dist/eslint/pasika/rules/component-placement.d.ts +11 -0
  23. package/dist/eslint/pasika/rules/component-placement.js +75 -0
  24. package/dist/eslint/pasika/rules/enforce-cva-variant-props.js +6 -1
  25. package/dist/eslint/pasika/rules/import-boundaries.js +53 -20
  26. package/dist/eslint/pasika/rules/no-arbitrary-tailwind.js +55 -65
  27. package/dist/eslint/pasika/rules/support-file-placement.d.ts +15 -0
  28. package/dist/eslint/pasika/rules/support-file-placement.js +70 -0
  29. package/enforcement/registry.json +1139 -0
  30. package/package.json +21 -4
@@ -16,6 +16,7 @@
16
16
  * the lookahead exclusion.
17
17
  */
18
18
  const ARBITRARY_VALUE_RE = /(?:^|(?<=\s))(?:[a-z]+(?:-[a-z]+)*)-\[[^\]]+\](?![-\w]*:)/g;
19
+ const CLASS_HELPERS = new Set(["cn", "clsx", "twMerge", "twJoin"]);
19
20
  export const noArbitraryTailwindRule = {
20
21
  meta: {
21
22
  schema: [],
@@ -25,83 +26,72 @@ export const noArbitraryTailwindRule = {
25
26
  },
26
27
  },
27
28
  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
- });
29
+ function reportClassString(node, value) {
30
+ ARBITRARY_VALUE_RE.lastIndex = 0;
31
+ const match = ARBITRARY_VALUE_RE.exec(value);
32
+ if (!match)
33
+ return;
34
+ context.report({
35
+ node,
36
+ message: `Tailwind arbitrary-value class "${match[0]}" is not allowed. ` +
37
+ "Use a named token or custom utility. " +
38
+ "See docs/styling-guide/rules/arbitrary-value-rule.md",
39
+ });
40
+ }
41
+ /** Walks every expression a class name can hide in: conditionals, arrays, and object keys. */
42
+ function checkExpression(node, expression) {
43
+ if (!expression)
44
+ return;
45
+ if (expression.type === "Literal") {
46
+ if (typeof expression.value === "string")
47
+ reportClassString(node, expression.value);
48
+ return;
49
+ }
50
+ if (expression.type === "TemplateLiteral") {
51
+ for (const quasi of expression.quasis)
52
+ reportClassString(node, quasi.value.raw);
53
+ return;
54
+ }
55
+ if (expression.type === "LogicalExpression") {
56
+ checkExpression(node, expression.left);
57
+ checkExpression(node, expression.right);
58
+ return;
59
+ }
60
+ if (expression.type === "ConditionalExpression") {
61
+ checkExpression(node, expression.consequent);
62
+ checkExpression(node, expression.alternate);
63
+ return;
64
+ }
65
+ if (expression.type === "ArrayExpression") {
66
+ for (const element of expression.elements)
67
+ checkExpression(node, element);
68
+ return;
69
+ }
70
+ if (expression.type === "ObjectExpression") {
71
+ // clsx and cn accept `{ "px-[3px]": isActive }`, so the keys carry classes.
72
+ for (const property of expression.properties) {
73
+ if (property.type === "Property")
74
+ checkExpression(node, property.key);
39
75
  }
40
76
  }
41
77
  }
42
78
  return {
43
79
  JSXAttribute(node) {
44
- if (node.name.name !== "className" && node.name.name !== "class")
80
+ const attributeName = node.name?.name;
81
+ if (attributeName !== "className" && attributeName !== "class")
45
82
  return;
46
- const valueNode = node.value;
47
- if (!valueNode)
83
+ const value = node.value;
84
+ if (!value)
48
85
  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
- }
86
+ checkExpression(node, value.type === "JSXExpressionContainer" ? value.expression : value);
79
87
  },
80
88
  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")
89
+ if (node.callee.type !== "Identifier" || !CLASS_HELPERS.has(node.callee.name))
85
90
  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
- }
91
+ for (const argument of node.arguments) {
92
+ checkExpression(node, argument);
102
93
  }
103
94
  },
104
95
  };
105
96
  },
106
97
  };
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,15 @@
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;
@@ -0,0 +1,70 @@
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
+ };