pasika 0.3.1 → 0.3.3
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 +67 -9
- package/dist/cli/index.js +34 -40
- package/dist/cli/output.d.ts +3 -0
- package/dist/cli/output.js +11 -0
- package/dist/enforcement/coverage.d.ts +5 -1
- package/dist/enforcement/coverage.js +68 -39
- package/dist/enforcement/doctor.d.ts +16 -0
- package/dist/enforcement/doctor.js +249 -0
- package/dist/enforcement/parse-docs.js +5 -1
- package/dist/enforcement/types.d.ts +0 -3
- package/dist/enforcement/types.js +4 -13
- package/dist/eslint/pasika/ast-types.d.ts +122 -0
- package/dist/eslint/pasika/ast-types.js +1 -0
- package/dist/eslint/pasika/index.d.ts +58 -3
- package/dist/eslint/pasika/index.js +61 -1
- package/dist/eslint/pasika/rules/application-structure.d.ts +2 -0
- package/dist/eslint/pasika/rules/application-structure.js +152 -0
- package/dist/eslint/pasika/rules/component-conventions.d.ts +16 -0
- package/dist/eslint/pasika/rules/component-conventions.js +145 -0
- package/dist/eslint/pasika/rules/component-nesting.d.ts +12 -0
- package/dist/eslint/pasika/rules/component-nesting.js +69 -0
- package/dist/eslint/pasika/rules/config-extraction.d.ts +14 -0
- package/dist/eslint/pasika/rules/config-extraction.js +74 -0
- package/dist/eslint/pasika/rules/cross-feature-import.d.ts +2 -0
- package/dist/eslint/pasika/rules/cross-feature-import.js +75 -0
- package/dist/eslint/pasika/rules/css/apply-usage.d.ts +10 -0
- package/dist/eslint/pasika/rules/css/apply-usage.js +45 -0
- package/dist/eslint/pasika/rules/css/base-layer-pair.d.ts +10 -0
- package/dist/eslint/pasika/rules/css/base-layer-pair.js +49 -0
- package/dist/eslint/pasika/rules/css/css-variable-naming.d.ts +10 -0
- package/dist/eslint/pasika/rules/css/css-variable-naming.js +39 -0
- package/dist/eslint/pasika/rules/css/custom-utility-apply.d.ts +11 -0
- package/dist/eslint/pasika/rules/css/custom-utility-apply.js +52 -0
- package/dist/eslint/pasika/rules/css/global-css-location.d.ts +10 -0
- package/dist/eslint/pasika/rules/css/global-css-location.js +42 -0
- package/dist/eslint/pasika/rules/css/helpers.d.ts +29 -0
- package/dist/eslint/pasika/rules/css/helpers.js +93 -0
- package/dist/eslint/pasika/rules/css/index.d.ts +13 -0
- package/dist/eslint/pasika/rules/css/index.js +25 -0
- package/dist/eslint/pasika/rules/css/root-variables.d.ts +11 -0
- package/dist/eslint/pasika/rules/css/root-variables.js +53 -0
- package/dist/eslint/pasika/rules/css/rule-tester.d.ts +11 -0
- package/dist/eslint/pasika/rules/css/rule-tester.js +22 -0
- package/dist/eslint/pasika/rules/css/stylesheet-ordering.d.ts +11 -0
- package/dist/eslint/pasika/rules/css/stylesheet-ordering.js +76 -0
- package/dist/eslint/pasika/rules/css/surface-utility.d.ts +10 -0
- package/dist/eslint/pasika/rules/css/surface-utility.js +41 -0
- package/dist/eslint/pasika/rules/css/theme-reset.d.ts +9 -0
- package/dist/eslint/pasika/rules/css/theme-reset.js +36 -0
- package/dist/eslint/pasika/rules/css/theme-variable-namespace.d.ts +11 -0
- package/dist/eslint/pasika/rules/css/theme-variable-namespace.js +53 -0
- package/dist/eslint/pasika/rules/cva-appearance-props.d.ts +10 -0
- package/dist/eslint/pasika/rules/cva-appearance-props.js +77 -0
- package/dist/eslint/pasika/rules/cva-boolean-variants.d.ts +11 -0
- package/dist/eslint/pasika/rules/cva-boolean-variants.js +97 -0
- package/dist/eslint/pasika/rules/data-testid-case.d.ts +2 -0
- package/dist/eslint/pasika/rules/data-testid-case.js +76 -0
- package/dist/eslint/pasika/rules/enforce-cn-merge.d.ts +13 -1
- package/dist/eslint/pasika/rules/enforce-cn-merge.js +46 -44
- package/dist/eslint/pasika/rules/enforce-cva-variant-props.js +53 -48
- package/dist/eslint/pasika/rules/filename-case.js +28 -5
- package/dist/eslint/pasika/rules/hook-complexity.d.ts +12 -0
- package/dist/eslint/pasika/rules/hook-complexity.js +114 -0
- package/dist/eslint/pasika/rules/hook-extraction.d.ts +11 -0
- package/dist/eslint/pasika/rules/hook-extraction.js +55 -0
- package/dist/eslint/pasika/rules/import-through-index.d.ts +2 -0
- package/dist/eslint/pasika/rules/import-through-index.js +52 -0
- package/dist/eslint/pasika/rules/interactive-component.d.ts +10 -0
- package/dist/eslint/pasika/rules/interactive-component.js +76 -0
- package/dist/eslint/pasika/rules/json/index.d.ts +5 -0
- package/dist/eslint/pasika/rules/json/index.js +9 -0
- package/dist/eslint/pasika/rules/json/no-cache-flag.d.ts +10 -0
- package/dist/eslint/pasika/rules/json/no-cache-flag.js +45 -0
- package/dist/eslint/pasika/rules/json/no-vulyk-dependency.d.ts +10 -0
- package/dist/eslint/pasika/rules/json/no-vulyk-dependency.js +41 -0
- package/dist/eslint/pasika/rules/json/rule-tester.d.ts +11 -0
- package/dist/eslint/pasika/rules/json/rule-tester.js +21 -0
- package/dist/eslint/pasika/rules/jsx-hygiene.d.ts +9 -0
- package/dist/eslint/pasika/rules/jsx-hygiene.js +108 -0
- package/dist/eslint/pasika/rules/locale-dotted-path.d.ts +10 -0
- package/dist/eslint/pasika/rules/locale-dotted-path.js +65 -0
- package/dist/eslint/pasika/rules/locale-placement.d.ts +13 -0
- package/dist/eslint/pasika/rules/locale-placement.js +163 -0
- package/dist/eslint/pasika/rules/locales-location.d.ts +11 -0
- package/dist/eslint/pasika/rules/locales-location.js +56 -0
- package/dist/eslint/pasika/rules/md/doc-kind-suffix.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/doc-kind-suffix.js +29 -0
- package/dist/eslint/pasika/rules/md/example-heading-description.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/example-heading-description.js +30 -0
- package/dist/eslint/pasika/rules/md/glossary-term-linking.d.ts +2 -0
- package/dist/eslint/pasika/rules/md/glossary-term-linking.js +99 -0
- package/dist/eslint/pasika/rules/md/guide-folder-entry-point.d.ts +2 -0
- package/dist/eslint/pasika/rules/md/guide-folder-entry-point.js +46 -0
- package/dist/eslint/pasika/rules/md/guide-link-anchors.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/guide-link-anchors.js +50 -0
- package/dist/eslint/pasika/rules/md/guide-overview-no-links.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/guide-overview-no-links.js +51 -0
- package/dist/eslint/pasika/rules/md/guide-states-no-requirement.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/guide-states-no-requirement.js +26 -0
- package/dist/eslint/pasika/rules/md/guide-step-single-link.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/guide-step-single-link.js +48 -0
- package/dist/eslint/pasika/rules/md/guide-step-single-sentence.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/guide-step-single-sentence.js +42 -0
- package/dist/eslint/pasika/rules/md/helpers.d.ts +25 -0
- package/dist/eslint/pasika/rules/md/helpers.js +37 -0
- package/dist/eslint/pasika/rules/md/index.d.ts +27 -0
- package/dist/eslint/pasika/rules/md/index.js +53 -0
- package/dist/eslint/pasika/rules/md/no-cross-document-link.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/no-cross-document-link.js +36 -0
- package/dist/eslint/pasika/rules/md/no-nested-how-to.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/no-nested-how-to.js +39 -0
- package/dist/eslint/pasika/rules/md/no-template-prompt.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/no-template-prompt.js +22 -0
- package/dist/eslint/pasika/rules/md/overview-length.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/overview-length.js +47 -0
- package/dist/eslint/pasika/rules/md/overview-present.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/overview-present.js +41 -0
- package/dist/eslint/pasika/rules/md/policy-no-examples.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/policy-no-examples.js +28 -0
- package/dist/eslint/pasika/rules/md/policy-single-document.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/policy-single-document.js +34 -0
- package/dist/eslint/pasika/rules/md/policy-subject-headings.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/policy-subject-headings.js +58 -0
- package/dist/eslint/pasika/rules/md/project-index.d.ts +17 -0
- package/dist/eslint/pasika/rules/md/project-index.js +67 -0
- package/dist/eslint/pasika/rules/md/reference-block-headings.d.ts +6 -0
- package/dist/eslint/pasika/rules/md/reference-block-headings.js +31 -0
- package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.d.ts +6 -0
- package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.js +26 -0
- package/dist/eslint/pasika/rules/md/requirement-present.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/requirement-present.js +41 -0
- package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.js +46 -0
- package/dist/eslint/pasika/rules/md/rule-paired-examples.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/rule-paired-examples.js +36 -0
- package/dist/eslint/pasika/rules/md/rule-tester.d.ts +11 -0
- package/dist/eslint/pasika/rules/md/rule-tester.js +21 -0
- package/dist/eslint/pasika/rules/md/support-document-placement.d.ts +2 -0
- package/dist/eslint/pasika/rules/md/support-document-placement.js +36 -0
- package/dist/eslint/pasika/rules/md/title-matches-file-name.d.ts +2 -0
- package/dist/eslint/pasika/rules/md/title-matches-file-name.js +46 -0
- package/dist/eslint/pasika/rules/named-exports.d.ts +2 -0
- package/dist/eslint/pasika/rules/named-exports.js +49 -0
- package/dist/eslint/pasika/rules/no-eslint-disable.d.ts +10 -0
- package/dist/eslint/pasika/rules/no-eslint-disable.js +33 -0
- package/dist/eslint/pasika/rules/no-mixed-concerns.js +9 -22
- package/dist/eslint/pasika/rules/no-util-barrel.d.ts +2 -0
- package/dist/eslint/pasika/rules/no-util-barrel.js +48 -0
- package/dist/eslint/pasika/rules/pure-function-extract.d.ts +9 -0
- package/dist/eslint/pasika/rules/pure-function-extract.js +92 -0
- package/dist/eslint/pasika/rules/shared-style-dedup.d.ts +13 -0
- package/dist/eslint/pasika/rules/shared-style-dedup.js +91 -0
- package/dist/eslint/pasika/rules/stay-flat.d.ts +12 -0
- package/dist/eslint/pasika/rules/stay-flat.js +81 -0
- package/dist/eslint/pasika/rules/support-folder-shape.d.ts +2 -0
- package/dist/eslint/pasika/rules/support-folder-shape.js +59 -0
- package/dist/eslint/pasika/rules/type-extraction.d.ts +13 -0
- package/dist/eslint/pasika/rules/type-extraction.js +85 -0
- package/dist/eslint/pasika/rules/ui-state.d.ts +9 -0
- package/dist/eslint/pasika/rules/ui-state.js +99 -0
- package/dist/eslint/pasika/rules/util-file-name.d.ts +2 -0
- package/dist/eslint/pasika/rules/util-file-name.js +48 -0
- package/dist/eslint/pasika/rules/value-extraction.d.ts +13 -0
- package/dist/eslint/pasika/rules/value-extraction.js +48 -0
- package/package.json +14 -7
- package/dist/enforcement/docs-check.d.ts +0 -17
- package/dist/enforcement/docs-check.js +0 -162
- package/enforcement/registry.json +0 -1139
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/base-layer-pair
|
|
3
|
+
*
|
|
4
|
+
* The global base layer MUST apply base-canvas and base-ink to the document
|
|
5
|
+
* body as the default page pair.
|
|
6
|
+
*
|
|
7
|
+
* @see docs/styling-guide/rules/global-stylesheet-rule.md
|
|
8
|
+
*/
|
|
9
|
+
import { atrulesNamed, blockChildren, preludeIdentifiers, selectorNames } from "./helpers.js";
|
|
10
|
+
export const baseLayerPairRule = {
|
|
11
|
+
meta: {
|
|
12
|
+
schema: [],
|
|
13
|
+
type: "problem",
|
|
14
|
+
docs: {
|
|
15
|
+
description: "Require the base layer to apply base-canvas and base-ink to the body.",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
"StyleSheet:exit"(node) {
|
|
21
|
+
const baseLayers = atrulesNamed(node, "layer").filter((layer) => preludeIdentifiers(layer).includes("base"));
|
|
22
|
+
if (baseLayers.length === 0)
|
|
23
|
+
return;
|
|
24
|
+
const applied = [];
|
|
25
|
+
for (const layer of baseLayers) {
|
|
26
|
+
for (const child of blockChildren(layer)) {
|
|
27
|
+
if (child.type !== "Rule")
|
|
28
|
+
continue;
|
|
29
|
+
if (!selectorNames(child).includes("body"))
|
|
30
|
+
continue;
|
|
31
|
+
for (const declaration of blockChildren(child)) {
|
|
32
|
+
if (declaration.type !== "Atrule" || declaration.name !== "apply")
|
|
33
|
+
continue;
|
|
34
|
+
applied.push(...preludeIdentifiers(declaration));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const hasCanvas = applied.some((name) => name.includes("canvas"));
|
|
39
|
+
const hasInk = applied.some((name) => name.includes("ink"));
|
|
40
|
+
if (!hasCanvas || !hasInk) {
|
|
41
|
+
context.report({
|
|
42
|
+
node,
|
|
43
|
+
message: "The global base layer must apply a canvas and an ink utility to the document body as the default page pair.",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/css-variable-naming
|
|
3
|
+
*
|
|
4
|
+
* A CSS variable intended only for a background MUST be named --<role>-canvas,
|
|
5
|
+
* and one intended only for readable text MUST be named --<role>-ink.
|
|
6
|
+
*
|
|
7
|
+
* @see docs/styling-guide/rules/theme-and-utility-definition-rule.md
|
|
8
|
+
*/
|
|
9
|
+
import type { CSSRuleDefinition } from "@eslint/css";
|
|
10
|
+
export declare const cssVariableNamingRule: CSSRuleDefinition;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/css-variable-naming
|
|
3
|
+
*
|
|
4
|
+
* A CSS variable intended only for a background MUST be named --<role>-canvas,
|
|
5
|
+
* and one intended only for readable text MUST be named --<role>-ink.
|
|
6
|
+
*
|
|
7
|
+
* @see docs/styling-guide/rules/theme-and-utility-definition-rule.md
|
|
8
|
+
*/
|
|
9
|
+
export const cssVariableNamingRule = {
|
|
10
|
+
meta: {
|
|
11
|
+
schema: [],
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Require background variables to end in -canvas and text variables to end in -ink.",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
Declaration(node) {
|
|
20
|
+
const property = node.property;
|
|
21
|
+
if (!property.startsWith("--"))
|
|
22
|
+
return;
|
|
23
|
+
const lower = property.toLowerCase();
|
|
24
|
+
if ((lower.includes("bg") || lower.includes("background")) && !lower.endsWith("-canvas")) {
|
|
25
|
+
context.report({
|
|
26
|
+
node,
|
|
27
|
+
message: `CSS variable "${property}" looks like a background token; name it --<role>-canvas instead.`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (lower.includes("text") && !lower.endsWith("-ink")) {
|
|
31
|
+
context.report({
|
|
32
|
+
node,
|
|
33
|
+
message: `CSS variable "${property}" looks like a text token; name it --<role>-ink instead.`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/custom-utility-apply
|
|
3
|
+
*
|
|
4
|
+
* A custom utility MUST use @apply for every styling declaration added by the
|
|
5
|
+
* project. When no named built-in utility represents a property value, it MUST
|
|
6
|
+
* apply the Tailwind custom-property or arbitrary-property utility instead.
|
|
7
|
+
*
|
|
8
|
+
* @see docs/styling-guide/rules/theme-and-utility-definition-rule.md
|
|
9
|
+
*/
|
|
10
|
+
import type { CSSRuleDefinition } from "@eslint/css";
|
|
11
|
+
export declare const customUtilityApplyRule: CSSRuleDefinition;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/custom-utility-apply
|
|
3
|
+
*
|
|
4
|
+
* A custom utility MUST use @apply for every styling declaration added by the
|
|
5
|
+
* project. When no named built-in utility represents a property value, it MUST
|
|
6
|
+
* apply the Tailwind custom-property or arbitrary-property utility instead.
|
|
7
|
+
*
|
|
8
|
+
* @see docs/styling-guide/rules/theme-and-utility-definition-rule.md
|
|
9
|
+
*/
|
|
10
|
+
import { atrulesNamed, blockChildren } from "./helpers.js";
|
|
11
|
+
/** The property name when a tolerant-mode `Raw` is actually `property: value`. */
|
|
12
|
+
function rawProperty(raw) {
|
|
13
|
+
const match = /^\s*(?<property>[a-z][a-z-]*)\s*:/.exec(raw);
|
|
14
|
+
return match?.groups?.property;
|
|
15
|
+
}
|
|
16
|
+
export const customUtilityApplyRule = {
|
|
17
|
+
meta: {
|
|
18
|
+
schema: [],
|
|
19
|
+
type: "problem",
|
|
20
|
+
docs: {
|
|
21
|
+
description: "Require @utility blocks to style through @apply.",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
return {
|
|
26
|
+
"StyleSheet:exit"(node) {
|
|
27
|
+
for (const utility of atrulesNamed(node, "utility")) {
|
|
28
|
+
for (const child of blockChildren(utility)) {
|
|
29
|
+
if (child.type === "Atrule" && child.name === "apply")
|
|
30
|
+
continue;
|
|
31
|
+
if (child.type === "Declaration") {
|
|
32
|
+
context.report({
|
|
33
|
+
node: child,
|
|
34
|
+
message: `Custom utility must use @apply for styling declaration "${child.property}".`,
|
|
35
|
+
});
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (child.type === "Raw") {
|
|
39
|
+
const property = rawProperty(child.value);
|
|
40
|
+
if (property !== undefined) {
|
|
41
|
+
context.report({
|
|
42
|
+
node: child,
|
|
43
|
+
message: `Custom utility must use @apply for styling declaration "${property}".`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/global-css-location
|
|
3
|
+
*
|
|
4
|
+
* The project's global CSS MUST live in the global stylesheet entry point and
|
|
5
|
+
* MUST NOT be imported from another file.
|
|
6
|
+
*
|
|
7
|
+
* @see docs/styling-guide/rules/global-stylesheet-rule.md
|
|
8
|
+
*/
|
|
9
|
+
import type { CSSRuleDefinition } from "@eslint/css";
|
|
10
|
+
export declare const globalCssLocationRule: CSSRuleDefinition;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/global-css-location
|
|
3
|
+
*
|
|
4
|
+
* The project's global CSS MUST live in the global stylesheet entry point and
|
|
5
|
+
* MUST NOT be imported from another file.
|
|
6
|
+
*
|
|
7
|
+
* @see docs/styling-guide/rules/global-stylesheet-rule.md
|
|
8
|
+
*/
|
|
9
|
+
export const globalCssLocationRule = {
|
|
10
|
+
meta: {
|
|
11
|
+
schema: [],
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Require project global CSS to live only in the entry point stylesheet.",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
"StyleSheet:exit"(node) {
|
|
20
|
+
// The entry point registers Tailwind. Any other stylesheet with
|
|
21
|
+
// project CSS — or that imports the entry point — splits global CSS
|
|
22
|
+
// out of the entry point.
|
|
23
|
+
const registersTailwind = node.children.some((child) => child.type === "Atrule" && child.name === "import" && JSON.stringify(child.prelude).includes("tailwindcss"));
|
|
24
|
+
if (registersTailwind)
|
|
25
|
+
return;
|
|
26
|
+
const hasProjectCss = node.children.some((child) => {
|
|
27
|
+
if (child.type === "Atrule" && child.name === "import")
|
|
28
|
+
return false;
|
|
29
|
+
if (child.type === "Comment")
|
|
30
|
+
return false;
|
|
31
|
+
return true;
|
|
32
|
+
});
|
|
33
|
+
if (!hasProjectCss)
|
|
34
|
+
return;
|
|
35
|
+
context.report({
|
|
36
|
+
node,
|
|
37
|
+
message: "Global CSS must live in the global stylesheet entry point that registers Tailwind, not in this file.",
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared AST walkers for the CSS rules.
|
|
3
|
+
*
|
|
4
|
+
* The @eslint/css language parses with css-tree in tolerant mode, so constructs
|
|
5
|
+
* Tailwind v4 extends beyond the CSS grammar — `--*: initial`, arbitrary
|
|
6
|
+
* properties inside `@apply`, nested `@media` inside `@theme` — survive as
|
|
7
|
+
* `Raw` nodes instead of structured ones. Rules therefore walk the plain AST
|
|
8
|
+
* and fall back to raw text when a construct came through unparsed.
|
|
9
|
+
*/
|
|
10
|
+
import type { CssNodePlain } from "@eslint/css-tree";
|
|
11
|
+
/** Visits `node` and every descendant, in document order. */
|
|
12
|
+
export declare function walkNodes(node: CssNodePlain, visit: (current: CssNodePlain) => void): void;
|
|
13
|
+
/** Every `Atrule` node in the sheet, in document order. */
|
|
14
|
+
export declare function atrules(node: CssNodePlain): CssNodePlain[];
|
|
15
|
+
/** Every `Atrule` with the given name. */
|
|
16
|
+
export declare function atrulesNamed(node: CssNodePlain, name: string): CssNodePlain[];
|
|
17
|
+
/** The children of a block-bearing node. */
|
|
18
|
+
export declare function blockChildren(node: CssNodePlain | undefined): CssNodePlain[];
|
|
19
|
+
/**
|
|
20
|
+
* The names in an at-rule prelude: `Identifier`s (the classes of an `@apply`,
|
|
21
|
+
* the `inline` of `@theme inline`) and `Layer`s (the layers of `@layer base`).
|
|
22
|
+
*/
|
|
23
|
+
export declare function preludeIdentifiers(node: CssNodePlain | undefined): string[];
|
|
24
|
+
/** The text of a `Raw` node, or the empty string. */
|
|
25
|
+
export declare function rawValue(node: CssNodePlain | undefined): string;
|
|
26
|
+
/** Every `Rule` node in the sheet, in document order. */
|
|
27
|
+
export declare function rules(node: CssNodePlain): CssNodePlain[];
|
|
28
|
+
/** The selector names of a rule's prelude, e.g. `root` for `:root`. */
|
|
29
|
+
export declare function selectorNames(node: CssNodePlain | undefined): string[];
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared AST walkers for the CSS rules.
|
|
3
|
+
*
|
|
4
|
+
* The @eslint/css language parses with css-tree in tolerant mode, so constructs
|
|
5
|
+
* Tailwind v4 extends beyond the CSS grammar — `--*: initial`, arbitrary
|
|
6
|
+
* properties inside `@apply`, nested `@media` inside `@theme` — survive as
|
|
7
|
+
* `Raw` nodes instead of structured ones. Rules therefore walk the plain AST
|
|
8
|
+
* and fall back to raw text when a construct came through unparsed.
|
|
9
|
+
*/
|
|
10
|
+
/** Visits `node` and every descendant, in document order. */
|
|
11
|
+
export function walkNodes(node, visit) {
|
|
12
|
+
visit(node);
|
|
13
|
+
if ("children" in node && node.children) {
|
|
14
|
+
for (const child of node.children)
|
|
15
|
+
walkNodes(child, visit);
|
|
16
|
+
}
|
|
17
|
+
if ("prelude" in node && node.prelude) {
|
|
18
|
+
walkNodes(node.prelude, visit);
|
|
19
|
+
}
|
|
20
|
+
if ("block" in node && node.block) {
|
|
21
|
+
walkNodes(node.block, visit);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Every `Atrule` node in the sheet, in document order. */
|
|
25
|
+
export function atrules(node) {
|
|
26
|
+
const result = [];
|
|
27
|
+
walkNodes(node, (current) => {
|
|
28
|
+
if (current.type === "Atrule")
|
|
29
|
+
result.push(current);
|
|
30
|
+
});
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
/** Every `Atrule` with the given name. */
|
|
34
|
+
export function atrulesNamed(node, name) {
|
|
35
|
+
return atrules(node).filter((candidate) => candidate.type === "Atrule" && candidate.name === name);
|
|
36
|
+
}
|
|
37
|
+
/** The children of a block-bearing node. */
|
|
38
|
+
export function blockChildren(node) {
|
|
39
|
+
if (!node)
|
|
40
|
+
return [];
|
|
41
|
+
if (!("block" in node) || !node.block)
|
|
42
|
+
return [];
|
|
43
|
+
return node.block.children;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The names in an at-rule prelude: `Identifier`s (the classes of an `@apply`,
|
|
47
|
+
* the `inline` of `@theme inline`) and `Layer`s (the layers of `@layer base`).
|
|
48
|
+
*/
|
|
49
|
+
export function preludeIdentifiers(node) {
|
|
50
|
+
if (!node)
|
|
51
|
+
return [];
|
|
52
|
+
if (!("prelude" in node) || !node.prelude)
|
|
53
|
+
return [];
|
|
54
|
+
const names = [];
|
|
55
|
+
walkNodes(node.prelude, (current) => {
|
|
56
|
+
if (current.type === "Identifier")
|
|
57
|
+
names.push(current.name);
|
|
58
|
+
if (current.type === "Layer")
|
|
59
|
+
names.push(current.name);
|
|
60
|
+
});
|
|
61
|
+
return names;
|
|
62
|
+
}
|
|
63
|
+
/** The text of a `Raw` node, or the empty string. */
|
|
64
|
+
export function rawValue(node) {
|
|
65
|
+
if (!node)
|
|
66
|
+
return "";
|
|
67
|
+
if (node.type === "Raw")
|
|
68
|
+
return node.value;
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
/** Every `Rule` node in the sheet, in document order. */
|
|
72
|
+
export function rules(node) {
|
|
73
|
+
const result = [];
|
|
74
|
+
walkNodes(node, (current) => {
|
|
75
|
+
if (current.type === "Rule")
|
|
76
|
+
result.push(current);
|
|
77
|
+
});
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/** The selector names of a rule's prelude, e.g. `root` for `:root`. */
|
|
81
|
+
export function selectorNames(node) {
|
|
82
|
+
if (!node)
|
|
83
|
+
return [];
|
|
84
|
+
if (!("prelude" in node) || !node.prelude)
|
|
85
|
+
return [];
|
|
86
|
+
const names = [];
|
|
87
|
+
walkNodes(node.prelude, (current) => {
|
|
88
|
+
if (current.type === "PseudoClassSelector" || current.type === "TypeSelector" || current.type === "ClassSelector") {
|
|
89
|
+
names.push(current.name);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return names;
|
|
93
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const cssRules: {
|
|
2
|
+
"theme-reset": import("@eslint/css").CSSRuleDefinition;
|
|
3
|
+
"root-variables": import("@eslint/css").CSSRuleDefinition;
|
|
4
|
+
"apply-usage": import("@eslint/css").CSSRuleDefinition;
|
|
5
|
+
"base-layer-pair": import("@eslint/css").CSSRuleDefinition;
|
|
6
|
+
"stylesheet-ordering": import("@eslint/css").CSSRuleDefinition;
|
|
7
|
+
"css-variable-naming": import("@eslint/css").CSSRuleDefinition;
|
|
8
|
+
"custom-utility-apply": import("@eslint/css").CSSRuleDefinition;
|
|
9
|
+
"surface-utility": import("@eslint/css").CSSRuleDefinition;
|
|
10
|
+
"theme-variable-namespace": import("@eslint/css").CSSRuleDefinition;
|
|
11
|
+
"global-css-location": import("@eslint/css").CSSRuleDefinition;
|
|
12
|
+
};
|
|
13
|
+
export type CssRuleName = keyof typeof cssRules;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* All CSS rules for the global stylesheet checks.
|
|
3
|
+
*/
|
|
4
|
+
import { themeResetRule } from "./theme-reset.js";
|
|
5
|
+
import { rootVariablesRule } from "./root-variables.js";
|
|
6
|
+
import { applyUsageRule } from "./apply-usage.js";
|
|
7
|
+
import { baseLayerPairRule } from "./base-layer-pair.js";
|
|
8
|
+
import { stylesheetOrderingRule } from "./stylesheet-ordering.js";
|
|
9
|
+
import { cssVariableNamingRule } from "./css-variable-naming.js";
|
|
10
|
+
import { customUtilityApplyRule } from "./custom-utility-apply.js";
|
|
11
|
+
import { surfaceUtilityRule } from "./surface-utility.js";
|
|
12
|
+
import { themeVariableNamespaceRule } from "./theme-variable-namespace.js";
|
|
13
|
+
import { globalCssLocationRule } from "./global-css-location.js";
|
|
14
|
+
export const cssRules = {
|
|
15
|
+
"theme-reset": themeResetRule,
|
|
16
|
+
"root-variables": rootVariablesRule,
|
|
17
|
+
"apply-usage": applyUsageRule,
|
|
18
|
+
"base-layer-pair": baseLayerPairRule,
|
|
19
|
+
"stylesheet-ordering": stylesheetOrderingRule,
|
|
20
|
+
"css-variable-naming": cssVariableNamingRule,
|
|
21
|
+
"custom-utility-apply": customUtilityApplyRule,
|
|
22
|
+
"surface-utility": surfaceUtilityRule,
|
|
23
|
+
"theme-variable-namespace": themeVariableNamespaceRule,
|
|
24
|
+
"global-css-location": globalCssLocationRule,
|
|
25
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/root-variables
|
|
3
|
+
*
|
|
4
|
+
* Every value used for the project's styling MUST be defined as a CSS variable
|
|
5
|
+
* in :root, and a Tailwind theme variable MUST reference that CSS variable
|
|
6
|
+
* through @theme inline.
|
|
7
|
+
*
|
|
8
|
+
* @see docs/styling-guide/rules/global-stylesheet-rule.md
|
|
9
|
+
*/
|
|
10
|
+
import type { CSSRuleDefinition } from "@eslint/css";
|
|
11
|
+
export declare const rootVariablesRule: CSSRuleDefinition;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/root-variables
|
|
3
|
+
*
|
|
4
|
+
* Every value used for the project's styling MUST be defined as a CSS variable
|
|
5
|
+
* in :root, and a Tailwind theme variable MUST reference that CSS variable
|
|
6
|
+
* through @theme inline.
|
|
7
|
+
*
|
|
8
|
+
* @see docs/styling-guide/rules/global-stylesheet-rule.md
|
|
9
|
+
*/
|
|
10
|
+
import { atrulesNamed, blockChildren, preludeIdentifiers, rules, selectorNames } from "./helpers.js";
|
|
11
|
+
export const rootVariablesRule = {
|
|
12
|
+
meta: {
|
|
13
|
+
schema: [],
|
|
14
|
+
type: "problem",
|
|
15
|
+
docs: {
|
|
16
|
+
description: "Require styling values to be CSS variables in :root referenced through @theme inline.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
create(context) {
|
|
20
|
+
return {
|
|
21
|
+
"StyleSheet:exit"(node) {
|
|
22
|
+
const hasRootVars = rules(node).some((rule) => {
|
|
23
|
+
if (!selectorNames(rule).includes("root"))
|
|
24
|
+
return false;
|
|
25
|
+
return blockChildren(rule).some((child) => child.type === "Declaration");
|
|
26
|
+
});
|
|
27
|
+
if (!hasRootVars) {
|
|
28
|
+
context.report({
|
|
29
|
+
node,
|
|
30
|
+
message: "Styling values must be defined as CSS variables in a :root block.",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
// A Tailwind theme variable that references a CSS variable must go
|
|
34
|
+
// through @theme inline; a bare @theme block would generate values
|
|
35
|
+
// instead of referencing the :root variables.
|
|
36
|
+
const referencingThemes = atrulesNamed(node, "theme").filter((theme) => blockChildren(theme).some((child) => {
|
|
37
|
+
if (child.type === "Raw")
|
|
38
|
+
return child.value.includes("var(");
|
|
39
|
+
if (child.type === "Declaration")
|
|
40
|
+
return JSON.stringify(child.value).includes("var(");
|
|
41
|
+
return false;
|
|
42
|
+
}));
|
|
43
|
+
const inlineThemes = atrulesNamed(node, "theme").filter((theme) => preludeIdentifiers(theme).includes("inline"));
|
|
44
|
+
if (referencingThemes.length > 0 && inlineThemes.length === 0) {
|
|
45
|
+
context.report({
|
|
46
|
+
node,
|
|
47
|
+
message: "Tailwind theme variables that reference CSS variables must use @theme inline.",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared RuleTester wiring for the CSS rule tests.
|
|
3
|
+
*
|
|
4
|
+
* Each `describe` title in a rule test is the exact text of the requirement the
|
|
5
|
+
* case pins, which is how `pasika coverage` verifies that a requirement recorded
|
|
6
|
+
* as lint-enforced has a test behind it.
|
|
7
|
+
*/
|
|
8
|
+
import { describe } from "node:test";
|
|
9
|
+
import { RuleTester } from "eslint";
|
|
10
|
+
export declare const cssRuleTester: RuleTester;
|
|
11
|
+
export { describe };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared RuleTester wiring for the CSS rule tests.
|
|
3
|
+
*
|
|
4
|
+
* Each `describe` title in a rule test is the exact text of the requirement the
|
|
5
|
+
* case pins, which is how `pasika coverage` verifies that a requirement recorded
|
|
6
|
+
* as lint-enforced has a test behind it.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it } from "node:test";
|
|
9
|
+
import { RuleTester } from "eslint";
|
|
10
|
+
import css from "@eslint/css";
|
|
11
|
+
import { cssRules } from "../../index.js";
|
|
12
|
+
RuleTester.describe = describe;
|
|
13
|
+
RuleTester.it = it;
|
|
14
|
+
export const cssRuleTester = new RuleTester({
|
|
15
|
+
language: "css/css",
|
|
16
|
+
languageOptions: { tolerant: true },
|
|
17
|
+
plugins: {
|
|
18
|
+
css,
|
|
19
|
+
pasika: { rules: cssRules },
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
export { describe };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/stylesheet-ordering
|
|
3
|
+
*
|
|
4
|
+
* The global stylesheet MUST order imports, @custom-variant definitions, :root
|
|
5
|
+
* variables and the selectors that override them, @theme definitions, custom
|
|
6
|
+
* utilities, base styles, and keyframes in that order.
|
|
7
|
+
*
|
|
8
|
+
* @see docs/styling-guide/rules/global-stylesheet-rule.md
|
|
9
|
+
*/
|
|
10
|
+
import type { CSSRuleDefinition } from "@eslint/css";
|
|
11
|
+
export declare const stylesheetOrderingRule: CSSRuleDefinition;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/stylesheet-ordering
|
|
3
|
+
*
|
|
4
|
+
* The global stylesheet MUST order imports, @custom-variant definitions, :root
|
|
5
|
+
* variables and the selectors that override them, @theme definitions, custom
|
|
6
|
+
* utilities, base styles, and keyframes in that order.
|
|
7
|
+
*
|
|
8
|
+
* @see docs/styling-guide/rules/global-stylesheet-rule.md
|
|
9
|
+
*/
|
|
10
|
+
import { preludeIdentifiers, selectorNames } from "./helpers.js";
|
|
11
|
+
const SECTION_ORDER = ["imports", "custom-variant", "root", "theme", "utility", "base styles", "keyframes"];
|
|
12
|
+
export const stylesheetOrderingRule = {
|
|
13
|
+
meta: {
|
|
14
|
+
schema: [],
|
|
15
|
+
type: "problem",
|
|
16
|
+
docs: {
|
|
17
|
+
description: "Require the global stylesheet sections in the documented order.",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
return {
|
|
22
|
+
"StyleSheet:exit"(node) {
|
|
23
|
+
// Map each top-level child to its section name.
|
|
24
|
+
const sections = [];
|
|
25
|
+
let index = 0;
|
|
26
|
+
for (const child of node.children) {
|
|
27
|
+
if (child.type === "Atrule" && child.name === "import") {
|
|
28
|
+
sections.push({ name: "imports", at: index });
|
|
29
|
+
}
|
|
30
|
+
else if (child.type === "Atrule" && child.name === "custom-variant") {
|
|
31
|
+
sections.push({ name: "custom-variant", at: index });
|
|
32
|
+
}
|
|
33
|
+
else if (child.type === "Atrule" && child.name === "theme") {
|
|
34
|
+
sections.push({ name: "theme", at: index });
|
|
35
|
+
}
|
|
36
|
+
else if (child.type === "Atrule" && child.name === "utility") {
|
|
37
|
+
sections.push({ name: "utility", at: index });
|
|
38
|
+
}
|
|
39
|
+
else if (child.type === "Atrule" && child.name === "keyframes") {
|
|
40
|
+
sections.push({ name: "keyframes", at: index });
|
|
41
|
+
}
|
|
42
|
+
else if (child.type === "Atrule" && child.name === "layer") {
|
|
43
|
+
if (preludeIdentifiers(child).includes("base")) {
|
|
44
|
+
sections.push({ name: "base styles", at: index });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (child.type === "Rule" && selectorNames(child).includes("root")) {
|
|
48
|
+
sections.push({ name: "root", at: index });
|
|
49
|
+
}
|
|
50
|
+
index += 1;
|
|
51
|
+
}
|
|
52
|
+
// The first occurrence of each section must not appear after a later
|
|
53
|
+
// section has already started.
|
|
54
|
+
const firstAt = new Map();
|
|
55
|
+
for (const section of sections) {
|
|
56
|
+
if (!firstAt.has(section.name))
|
|
57
|
+
firstAt.set(section.name, section.at ?? 0);
|
|
58
|
+
}
|
|
59
|
+
let lastSeen = -1;
|
|
60
|
+
for (const name of SECTION_ORDER) {
|
|
61
|
+
const at = firstAt.get(name);
|
|
62
|
+
if (at === undefined)
|
|
63
|
+
continue;
|
|
64
|
+
if (at < lastSeen) {
|
|
65
|
+
context.report({
|
|
66
|
+
node,
|
|
67
|
+
message: `Global stylesheet sections must be ordered: imports, @custom-variant, :root, @theme, custom utilities, base styles, keyframes. Found "${name}" before an earlier section.`,
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
lastSeen = at;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/surface-utility
|
|
3
|
+
*
|
|
4
|
+
* A repeated combination of canvas, ink, and related styles MUST become a
|
|
5
|
+
* *-surface custom Tailwind utility that owns the combination.
|
|
6
|
+
*
|
|
7
|
+
* @see docs/styling-guide/rules/theme-and-utility-definition-rule.md
|
|
8
|
+
*/
|
|
9
|
+
import type { CSSRuleDefinition } from "@eslint/css";
|
|
10
|
+
export declare const surfaceUtilityRule: CSSRuleDefinition;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/surface-utility
|
|
3
|
+
*
|
|
4
|
+
* A repeated combination of canvas, ink, and related styles MUST become a
|
|
5
|
+
* *-surface custom Tailwind utility that owns the combination.
|
|
6
|
+
*
|
|
7
|
+
* @see docs/styling-guide/rules/theme-and-utility-definition-rule.md
|
|
8
|
+
*/
|
|
9
|
+
import { atrulesNamed, preludeIdentifiers } from "./helpers.js";
|
|
10
|
+
export const surfaceUtilityRule = {
|
|
11
|
+
meta: {
|
|
12
|
+
schema: [],
|
|
13
|
+
type: "problem",
|
|
14
|
+
docs: {
|
|
15
|
+
description: "Require repeated canvas+ink combinations to become a *-surface utility.",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
"StyleSheet:exit"(node) {
|
|
21
|
+
const combinations = new Map();
|
|
22
|
+
for (const apply of atrulesNamed(node, "apply")) {
|
|
23
|
+
const classes = preludeIdentifiers(apply);
|
|
24
|
+
if (!classes.some((name) => name.includes("canvas")) || !classes.some((name) => name.includes("ink"))) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const key = [...classes].sort().join(" ");
|
|
28
|
+
combinations.set(key, (combinations.get(key) ?? 0) + 1);
|
|
29
|
+
}
|
|
30
|
+
for (const [combo, count] of combinations) {
|
|
31
|
+
if (count < 2)
|
|
32
|
+
continue;
|
|
33
|
+
context.report({
|
|
34
|
+
node,
|
|
35
|
+
message: `Combination "${combo}" appears ${String(count)} times. Create a *-surface custom Tailwind utility for it.`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/theme-reset
|
|
3
|
+
*
|
|
4
|
+
* The global stylesheet MUST reset Tailwind's default theme with `--*: initial`.
|
|
5
|
+
*
|
|
6
|
+
* @see docs/styling-guide/rules/global-stylesheet-rule.md
|
|
7
|
+
*/
|
|
8
|
+
import type { CSSRuleDefinition } from "@eslint/css";
|
|
9
|
+
export declare const themeResetRule: CSSRuleDefinition;
|