pasika 0.3.4 → 0.3.6
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 +3 -0
- package/dist/cli/index.js +517 -159
- package/dist/eslint/pasika/index.d.ts +4 -2
- package/dist/eslint/pasika/index.js +471 -111
- package/package.json +8 -1
package/dist/cli/index.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
// cli/index.ts
|
|
4
4
|
import { existsSync } from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path41 from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// enforcement/coverage.ts
|
|
9
9
|
import { readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync, statSync as statSync5 } from "fs";
|
|
10
|
-
import
|
|
10
|
+
import path40 from "path";
|
|
11
11
|
import { z as z3 } from "zod";
|
|
12
12
|
|
|
13
13
|
// eslint/pasika/rules/filename-case.ts
|
|
@@ -598,8 +598,8 @@ var enforceCvaVariantPropsRule = {
|
|
|
598
598
|
if (node.typeAnnotation?.type !== "TSTypeLiteral") return;
|
|
599
599
|
for (const member of node.typeAnnotation.members ?? []) {
|
|
600
600
|
if (member.type !== "TSPropertySignature") continue;
|
|
601
|
-
const
|
|
602
|
-
if (!
|
|
601
|
+
const keyName2 = member.key?.type === "Identifier" ? member.key.name : void 0;
|
|
602
|
+
if (!keyName2) continue;
|
|
603
603
|
const typeAnn = member.typeAnnotation?.typeAnnotation;
|
|
604
604
|
if (typeAnn?.type !== "TSUnionType") continue;
|
|
605
605
|
const allStringLiterals = (typeAnn.types ?? []).every(
|
|
@@ -607,10 +607,10 @@ var enforceCvaVariantPropsRule = {
|
|
|
607
607
|
);
|
|
608
608
|
if (allStringLiterals && (typeAnn.types?.length ?? 0) >= 2) {
|
|
609
609
|
for (const variantNames of cvaDefinitions.values()) {
|
|
610
|
-
if (variantNames.includes(
|
|
610
|
+
if (variantNames.includes(keyName2)) {
|
|
611
611
|
context.report({
|
|
612
612
|
node,
|
|
613
|
-
message: `Variant prop "${
|
|
613
|
+
message: `Variant prop "${keyName2}" duplicates CVA variant values. Use VariantProps<typeof variants> instead of manually writing union types. See docs/styling-guide/rules/component-variant-rule.md`
|
|
614
614
|
});
|
|
615
615
|
return;
|
|
616
616
|
}
|
|
@@ -1346,21 +1346,27 @@ var dataTestIdCaseRule = {
|
|
|
1346
1346
|
Program(node) {
|
|
1347
1347
|
for (const component of components) {
|
|
1348
1348
|
const root = findSimpleRoot(component, text, filename);
|
|
1349
|
-
if (
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1349
|
+
if (root) {
|
|
1350
|
+
const { value } = getTestId(root);
|
|
1351
|
+
const expected = component.smart ? component.name : toKebabCase(component.name);
|
|
1352
|
+
if (component.smart && value === void 0) {
|
|
1353
|
+
context.report({
|
|
1354
|
+
node,
|
|
1355
|
+
message: `Smart component "${component.name}" with one outer DOM element must set data-testid="${expected}".`
|
|
1356
|
+
});
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
if (value === void 0 || value === expected) continue;
|
|
1360
|
+
if (component.smart ? !isPascalCase5(value) || value !== expected : !isKebabCase3(value)) {
|
|
1361
|
+
context.report({
|
|
1362
|
+
node,
|
|
1363
|
+
message: `data-testid for ${component.smart ? "smart" : "dumb"} component "${component.name}" must be ${component.smart ? "PascalCase" : "kebab-case"}: expected "${expected}".`
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
} else if (component.smart) {
|
|
1361
1367
|
context.report({
|
|
1362
1368
|
node,
|
|
1363
|
-
message: `
|
|
1369
|
+
message: `Smart component "${component.name}" has no single outer element; wrap its content in one outer element with data-testid="${component.name}" instead of rendering multiple roots. See docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md`
|
|
1364
1370
|
});
|
|
1365
1371
|
}
|
|
1366
1372
|
}
|
|
@@ -1843,14 +1849,14 @@ var cvaAppearancePropsRule = {
|
|
|
1843
1849
|
if (node.typeAnnotation?.type !== "TSTypeLiteral") return;
|
|
1844
1850
|
for (const member of node.typeAnnotation.members ?? []) {
|
|
1845
1851
|
if (member.type !== "TSPropertySignature") continue;
|
|
1846
|
-
const
|
|
1847
|
-
if (!
|
|
1852
|
+
const keyName2 = member.key?.type === "Identifier" ? member.key.name : void 0;
|
|
1853
|
+
if (!keyName2 || !VISUAL_OPTION_NAMES.has(keyName2)) continue;
|
|
1848
1854
|
const typeAnnotation = member.typeAnnotation?.typeAnnotation ?? member.typeAnnotation;
|
|
1849
1855
|
if (!typeAnnotation) continue;
|
|
1850
1856
|
if (typeAnnotation.type === "TSUnionType" || typeAnnotation.type === "TSTypeLiteral") {
|
|
1851
1857
|
context.report({
|
|
1852
1858
|
node,
|
|
1853
|
-
message: `Visual option prop "${
|
|
1859
|
+
message: `Visual option prop "${keyName2}" must be defined through a cva() call rather than a manual union type. See docs/styling-guide/rules/component-variant-rule.md`
|
|
1854
1860
|
});
|
|
1855
1861
|
}
|
|
1856
1862
|
}
|
|
@@ -1870,15 +1876,15 @@ function extractCvaInfo(node) {
|
|
|
1870
1876
|
if (options?.type !== "ObjectExpression") return { variantNames, compoundVariants };
|
|
1871
1877
|
for (const prop of options.properties) {
|
|
1872
1878
|
if (prop.type !== "Property") continue;
|
|
1873
|
-
const
|
|
1874
|
-
if (
|
|
1879
|
+
const keyName2 = prop.key.type === "Identifier" ? prop.key.name : void 0;
|
|
1880
|
+
if (keyName2 === "variants" && prop.value.type === "ObjectExpression") {
|
|
1875
1881
|
for (const vProp of prop.value.properties) {
|
|
1876
1882
|
if (vProp.type !== "Property") continue;
|
|
1877
1883
|
const vName = vProp.key.type === "Identifier" ? vProp.key.name : void 0;
|
|
1878
1884
|
if (vName) variantNames.add(vName);
|
|
1879
1885
|
}
|
|
1880
1886
|
}
|
|
1881
|
-
if (
|
|
1887
|
+
if (keyName2 === "compoundVariants" && prop.value.type === "ArrayExpression") {
|
|
1882
1888
|
for (const entry of prop.value.elements) {
|
|
1883
1889
|
if (entry?.type !== "ObjectExpression") continue;
|
|
1884
1890
|
for (const cvProp of entry.properties) {
|
|
@@ -2649,8 +2655,279 @@ var localePlacementRule = {
|
|
|
2649
2655
|
}
|
|
2650
2656
|
};
|
|
2651
2657
|
|
|
2652
|
-
// eslint/pasika/rules/
|
|
2658
|
+
// eslint/pasika/rules/sole-state-owner.ts
|
|
2653
2659
|
import path29 from "path";
|
|
2660
|
+
import ts6 from "typescript";
|
|
2661
|
+
function findStateHooks(node) {
|
|
2662
|
+
const hooks = [];
|
|
2663
|
+
const visit = (child) => {
|
|
2664
|
+
if (ts6.isCallExpression(child) && ts6.isIdentifier(child.expression) && child.expression.text === "useState") {
|
|
2665
|
+
if (ts6.isVariableDeclaration(child.parent)) {
|
|
2666
|
+
const { name } = child.parent;
|
|
2667
|
+
if (ts6.isArrayBindingPattern(name) && name.elements.length >= 2) {
|
|
2668
|
+
const value = name.elements[0];
|
|
2669
|
+
const updater = name.elements[1];
|
|
2670
|
+
if (value && updater && ts6.isBindingElement(value) && ts6.isBindingElement(updater)) {
|
|
2671
|
+
const valueName = value.name;
|
|
2672
|
+
const updaterName = updater.name;
|
|
2673
|
+
if (ts6.isIdentifier(valueName) && ts6.isIdentifier(updaterName)) {
|
|
2674
|
+
hooks.push({ value: valueName.text, updater: updaterName.text });
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
ts6.forEachChild(child, visit);
|
|
2681
|
+
};
|
|
2682
|
+
visit(node);
|
|
2683
|
+
return hooks;
|
|
2684
|
+
}
|
|
2685
|
+
function isHookUsage(node, hook) {
|
|
2686
|
+
if (ts6.isCallExpression(node) && ts6.isIdentifier(node.expression) && node.expression.text === hook.updater) {
|
|
2687
|
+
return "updater";
|
|
2688
|
+
}
|
|
2689
|
+
if (ts6.isIdentifier(node) && node.text === hook.value) {
|
|
2690
|
+
const parent = node.parent;
|
|
2691
|
+
if (ts6.isBindingElement(parent) || ts6.isPropertyAccessExpression(parent) || ts6.isShorthandPropertyAssignment(parent)) {
|
|
2692
|
+
return void 0;
|
|
2693
|
+
}
|
|
2694
|
+
return "value";
|
|
2695
|
+
}
|
|
2696
|
+
return void 0;
|
|
2697
|
+
}
|
|
2698
|
+
function topLevelJsxChildren(initial) {
|
|
2699
|
+
if (!initial) return void 0;
|
|
2700
|
+
let expression = initial;
|
|
2701
|
+
while (ts6.isParenthesizedExpression(expression)) expression = expression.expression;
|
|
2702
|
+
if (ts6.isJsxFragment(expression)) {
|
|
2703
|
+
const children = expression.children.filter(
|
|
2704
|
+
(c) => !ts6.isJsxText(c) && !ts6.isJsxSpreadAttribute(c)
|
|
2705
|
+
);
|
|
2706
|
+
return { root: expression, children };
|
|
2707
|
+
}
|
|
2708
|
+
if (ts6.isJsxElement(expression)) {
|
|
2709
|
+
const children = expression.children.filter(
|
|
2710
|
+
(c) => !ts6.isJsxText(c) && !ts6.isJsxSpreadAttribute(c)
|
|
2711
|
+
);
|
|
2712
|
+
return { root: expression, children };
|
|
2713
|
+
}
|
|
2714
|
+
return void 0;
|
|
2715
|
+
}
|
|
2716
|
+
function collectUsesIn(child, hook) {
|
|
2717
|
+
const positions = [];
|
|
2718
|
+
let count = 0;
|
|
2719
|
+
const visit = (node) => {
|
|
2720
|
+
if (isHookUsage(node, hook)) {
|
|
2721
|
+
positions.push(node);
|
|
2722
|
+
count += 1;
|
|
2723
|
+
}
|
|
2724
|
+
if (ts6.isFunctionDeclaration(node) || ts6.isClassDeclaration(node)) return;
|
|
2725
|
+
ts6.forEachChild(node, visit);
|
|
2726
|
+
};
|
|
2727
|
+
visit(child);
|
|
2728
|
+
return { positions, count };
|
|
2729
|
+
}
|
|
2730
|
+
var soleStateOwnerRule = {
|
|
2731
|
+
meta: {
|
|
2732
|
+
schema: [],
|
|
2733
|
+
type: "problem",
|
|
2734
|
+
docs: {
|
|
2735
|
+
description: "Require extracting a JSX part that is the sole owner of a useState hook as a named component."
|
|
2736
|
+
}
|
|
2737
|
+
},
|
|
2738
|
+
create(context) {
|
|
2739
|
+
const filename = path29.resolve(context.filename);
|
|
2740
|
+
if (!filename.endsWith(".tsx") && !filename.endsWith(".jsx")) return {};
|
|
2741
|
+
const text = context.sourceCode.text;
|
|
2742
|
+
const components = parseComponentInfo(text, filename);
|
|
2743
|
+
return {
|
|
2744
|
+
Program(node) {
|
|
2745
|
+
for (const component of components) {
|
|
2746
|
+
const hooks = findStateHooks(component.declaration);
|
|
2747
|
+
for (const hook of hooks) {
|
|
2748
|
+
const result = analyzeSoleOwner(component.declaration, hook);
|
|
2749
|
+
if (!result) continue;
|
|
2750
|
+
const { run, totalTopLevel } = result;
|
|
2751
|
+
if (run === 0 || totalTopLevel === 0) continue;
|
|
2752
|
+
context.report({
|
|
2753
|
+
node,
|
|
2754
|
+
loc: { line: 1, column: 0 },
|
|
2755
|
+
message: `Component "${component.name}" uses state "${hook.value}" in ${String(run)} contiguous top-level JSX part${run === 1 ? "" : "s"}; extract that part into a named component that owns useState instead of reading it in the parent. See docs/code-organization-guide/rules/sole-state-owner-rule.md`
|
|
2756
|
+
});
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
};
|
|
2763
|
+
function analyzeSoleOwner(declaration, hook) {
|
|
2764
|
+
const body = ts6.isFunctionDeclaration(declaration) ? declaration.body : void 0;
|
|
2765
|
+
if (!body || !ts6.isBlock(body)) return void 0;
|
|
2766
|
+
const returns = [];
|
|
2767
|
+
const visit = (node) => {
|
|
2768
|
+
if (node !== body && (ts6.isFunctionLike(node) || ts6.isClassLike(node))) return;
|
|
2769
|
+
if (ts6.isReturnStatement(node)) returns.push(node);
|
|
2770
|
+
ts6.forEachChild(node, visit);
|
|
2771
|
+
};
|
|
2772
|
+
visit(body);
|
|
2773
|
+
if (returns.length !== 1) return void 0;
|
|
2774
|
+
const single = returns[0];
|
|
2775
|
+
if (!single?.expression || !ts6.isExpression(single.expression)) return void 0;
|
|
2776
|
+
const returnExpression = single.expression;
|
|
2777
|
+
const root = topLevelJsxChildren(returnExpression);
|
|
2778
|
+
if (!root || root.children.length === 0) return void 0;
|
|
2779
|
+
const totalTopLevel = root.children.length;
|
|
2780
|
+
const usagePerChild = root.children.map((child) => collectUsesIn(child, hook));
|
|
2781
|
+
const outsideJsx = usesOutsideJsx(declaration, hook, root.children);
|
|
2782
|
+
if (outsideJsx) return void 0;
|
|
2783
|
+
const usedLabels = usagePerChild.map((u) => u.count > 0);
|
|
2784
|
+
const firstUsed = usedLabels.findIndex((v) => v);
|
|
2785
|
+
const lastUsed = usedLabels.lastIndexOf(true);
|
|
2786
|
+
if (firstUsed === -1 || lastUsed === -1) return void 0;
|
|
2787
|
+
const windowHasUnused = usedLabels.slice(firstUsed, lastUsed + 1).some((v) => !v);
|
|
2788
|
+
if (windowHasUnused) return void 0;
|
|
2789
|
+
const run = lastUsed - firstUsed + 1;
|
|
2790
|
+
const hasOutside = firstUsed > 0 || lastUsed < totalTopLevel - 1;
|
|
2791
|
+
if (!hasOutside) return void 0;
|
|
2792
|
+
return { run, totalTopLevel };
|
|
2793
|
+
}
|
|
2794
|
+
function usesOutsideJsx(declaration, hook, children) {
|
|
2795
|
+
let outside = false;
|
|
2796
|
+
const visit = (node) => {
|
|
2797
|
+
if (outside) return;
|
|
2798
|
+
if (node !== declaration && (ts6.isFunctionDeclaration(node) || ts6.isClassDeclaration(node))) return;
|
|
2799
|
+
if (isHookUsage(node, hook)) {
|
|
2800
|
+
let current = node;
|
|
2801
|
+
let isInJsxChild = false;
|
|
2802
|
+
while (!ts6.isSourceFile(current)) {
|
|
2803
|
+
if (children.includes(current)) {
|
|
2804
|
+
isInJsxChild = true;
|
|
2805
|
+
break;
|
|
2806
|
+
}
|
|
2807
|
+
current = current.parent;
|
|
2808
|
+
}
|
|
2809
|
+
if (!isInJsxChild) outside = true;
|
|
2810
|
+
}
|
|
2811
|
+
ts6.forEachChild(node, visit);
|
|
2812
|
+
};
|
|
2813
|
+
visit(declaration);
|
|
2814
|
+
return outside;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
// eslint/pasika/rules/locale-key-shape.ts
|
|
2818
|
+
import path30 from "path";
|
|
2819
|
+
var MAX_KEY_LENGTH = 30;
|
|
2820
|
+
var ROLE_POSTFIXES = /* @__PURE__ */ new Set([
|
|
2821
|
+
"Button",
|
|
2822
|
+
"Link",
|
|
2823
|
+
"Dialog",
|
|
2824
|
+
"AlertDialog",
|
|
2825
|
+
"Tooltip",
|
|
2826
|
+
"Banner",
|
|
2827
|
+
"Alert",
|
|
2828
|
+
"Status",
|
|
2829
|
+
"Tab",
|
|
2830
|
+
"TabPanel",
|
|
2831
|
+
"Menu",
|
|
2832
|
+
"MenuItem",
|
|
2833
|
+
"MenuBar",
|
|
2834
|
+
"Checkbox",
|
|
2835
|
+
"Radio",
|
|
2836
|
+
"RadioGroup",
|
|
2837
|
+
"Switch",
|
|
2838
|
+
"ComboBox",
|
|
2839
|
+
"ListBox",
|
|
2840
|
+
"Option",
|
|
2841
|
+
"Search",
|
|
2842
|
+
"SearchBox",
|
|
2843
|
+
"Navigation",
|
|
2844
|
+
"ProgressBar",
|
|
2845
|
+
"Slider",
|
|
2846
|
+
"SpinButton",
|
|
2847
|
+
"TextBox",
|
|
2848
|
+
"Tree",
|
|
2849
|
+
"TreeItem",
|
|
2850
|
+
"TreeGrid",
|
|
2851
|
+
"Grid",
|
|
2852
|
+
"GridCell",
|
|
2853
|
+
"Feed",
|
|
2854
|
+
"Log",
|
|
2855
|
+
"Timer",
|
|
2856
|
+
"Marquee",
|
|
2857
|
+
"Region",
|
|
2858
|
+
"Main",
|
|
2859
|
+
"Complementary",
|
|
2860
|
+
"ContentInfo",
|
|
2861
|
+
"Form",
|
|
2862
|
+
"Toolbar",
|
|
2863
|
+
"ScrollBar"
|
|
2864
|
+
]);
|
|
2865
|
+
var CAMEL_CASE = /^[a-z][a-zA-Z0-9]*$/;
|
|
2866
|
+
function isLocalesFile2(filename) {
|
|
2867
|
+
const segments = path30.resolve(filename).split(path30.sep);
|
|
2868
|
+
const srcIdx = segments.lastIndexOf("src");
|
|
2869
|
+
return srcIdx !== -1 && segments[srcIdx + 1] === "locales";
|
|
2870
|
+
}
|
|
2871
|
+
function keyName(key) {
|
|
2872
|
+
if (key.type === "Identifier") return key.name;
|
|
2873
|
+
if (key.type === "Literal" && typeof key.value === "string") return key.value;
|
|
2874
|
+
return void 0;
|
|
2875
|
+
}
|
|
2876
|
+
function reportFor(context, node, message) {
|
|
2877
|
+
context.report({ node, message });
|
|
2878
|
+
}
|
|
2879
|
+
function checkKey(context, node, name) {
|
|
2880
|
+
if (!CAMEL_CASE.test(name)) {
|
|
2881
|
+
reportFor(
|
|
2882
|
+
context,
|
|
2883
|
+
node,
|
|
2884
|
+
`Locale key "${name}" must be camelCase English based on the text. See docs/code-organization-guide/rules/locales-rule.md`
|
|
2885
|
+
);
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
if (name.length <= MAX_KEY_LENGTH) return;
|
|
2889
|
+
const hasRolePostfix = [...ROLE_POSTFIXES].some((role) => name.endsWith(role));
|
|
2890
|
+
if (hasRolePostfix) return;
|
|
2891
|
+
reportFor(
|
|
2892
|
+
context,
|
|
2893
|
+
node,
|
|
2894
|
+
`Locale key "${name}" is longer than ${String(MAX_KEY_LENGTH)} characters, so it must describe the message's purpose instead of the text; end it with an element role such as "Button", "Link", or "Dialog" (see WAI-ARIA roles). See docs/code-organization-guide/rules/locales-rule.md`
|
|
2895
|
+
);
|
|
2896
|
+
}
|
|
2897
|
+
var localeKeyShapeRule = {
|
|
2898
|
+
meta: {
|
|
2899
|
+
schema: [],
|
|
2900
|
+
type: "problem",
|
|
2901
|
+
docs: {
|
|
2902
|
+
description: "Require locale keys to be camelCase, and purpose-based element roles when long."
|
|
2903
|
+
}
|
|
2904
|
+
},
|
|
2905
|
+
create(context) {
|
|
2906
|
+
if (!isLocalesFile2(context.filename)) return {};
|
|
2907
|
+
function visitObject(node) {
|
|
2908
|
+
for (const property of node.properties) {
|
|
2909
|
+
if (property.type !== "Property") continue;
|
|
2910
|
+
const name = keyName(property.key);
|
|
2911
|
+
if (name === void 0) continue;
|
|
2912
|
+
if (property.value.type === "ObjectExpression") {
|
|
2913
|
+
visitObject(property.value);
|
|
2914
|
+
} else if (property.value.type === "Literal" && typeof property.value.value === "string") {
|
|
2915
|
+
checkKey(context, property, name);
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
return {
|
|
2920
|
+
VariableDeclarator(node) {
|
|
2921
|
+
if (node.id.type !== "Identifier" || node.id.name !== "locales") return;
|
|
2922
|
+
if (node.init?.type !== "ObjectExpression") return;
|
|
2923
|
+
visitObject(node.init);
|
|
2924
|
+
}
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
};
|
|
2928
|
+
|
|
2929
|
+
// eslint/pasika/rules/shared-style-dedup.ts
|
|
2930
|
+
import path31 from "path";
|
|
2654
2931
|
import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
2655
2932
|
var CLASS_NAME = /className="(?<classes>[^"]+)"/g;
|
|
2656
2933
|
var comboCache;
|
|
@@ -2691,8 +2968,8 @@ var sharedStyleDedupRule = {
|
|
|
2691
2968
|
}
|
|
2692
2969
|
},
|
|
2693
2970
|
create(context) {
|
|
2694
|
-
const sourceRoot2 =
|
|
2695
|
-
const file =
|
|
2971
|
+
const sourceRoot2 = path31.resolve("src");
|
|
2972
|
+
const file = path31.resolve(context.filename);
|
|
2696
2973
|
const segments = segmentsOf(file, sourceRoot2);
|
|
2697
2974
|
if (segments.length === 0) return {};
|
|
2698
2975
|
const index = getProjectIndex(sourceRoot2);
|
|
@@ -2714,28 +2991,111 @@ var sharedStyleDedupRule = {
|
|
|
2714
2991
|
}
|
|
2715
2992
|
};
|
|
2716
2993
|
|
|
2717
|
-
// eslint/pasika/rules/
|
|
2718
|
-
|
|
2994
|
+
// eslint/pasika/rules/repeated-structure.ts
|
|
2995
|
+
function nodeKind(node) {
|
|
2996
|
+
return node.type;
|
|
2997
|
+
}
|
|
2998
|
+
function isJsxNode(node) {
|
|
2999
|
+
const kind = nodeKind(node);
|
|
3000
|
+
return kind === "JSXElement" || kind === "JSXFragment";
|
|
3001
|
+
}
|
|
3002
|
+
function isJsxExpressionContainer(node) {
|
|
3003
|
+
return node.type === "JSXExpressionContainer";
|
|
3004
|
+
}
|
|
3005
|
+
function walkJsx(current, visit) {
|
|
3006
|
+
if (isJsxNode(current)) {
|
|
3007
|
+
visit(current);
|
|
3008
|
+
for (const child of current.children ?? []) walkJsx(child, visit);
|
|
3009
|
+
} else if (isJsxExpressionContainer(current)) {
|
|
3010
|
+
if (current.expression) walkJsx(current.expression, visit);
|
|
3011
|
+
} else if (current.type === "ArrowFunctionExpression") {
|
|
3012
|
+
walkJsx(current.body, visit);
|
|
3013
|
+
} else if (current.type === "CallExpression") {
|
|
3014
|
+
for (const argument of current.arguments) walkJsx(argument, visit);
|
|
3015
|
+
} else if (current.type === "ConditionalExpression") {
|
|
3016
|
+
walkJsx(current.consequent, visit);
|
|
3017
|
+
walkJsx(current.alternate, visit);
|
|
3018
|
+
} else if (current.type === "LogicalExpression") {
|
|
3019
|
+
walkJsx(current.left, visit);
|
|
3020
|
+
walkJsx(current.right, visit);
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
function signature(node) {
|
|
3024
|
+
const parts = [];
|
|
3025
|
+
const visit = (current) => {
|
|
3026
|
+
const opening = nodeKind(current) === "JSXFragment" ? "<>" : current.openingElement?.name.name ?? "<>";
|
|
3027
|
+
parts.push(opening);
|
|
3028
|
+
if (nodeKind(current) === "JSXElement") {
|
|
3029
|
+
const attributes = (current.openingElement?.attributes ?? []).filter((attribute) => attribute.type === "JSXAttribute").map((attribute) => {
|
|
3030
|
+
const name = attribute.name?.name ?? "";
|
|
3031
|
+
if (name === "className" && attribute.value?.type === "Literal" && typeof attribute.value.value === "string") {
|
|
3032
|
+
return `class="${attribute.value.value.split(/\s+/).filter(Boolean).sort().join(" ")}"`;
|
|
3033
|
+
}
|
|
3034
|
+
return name;
|
|
3035
|
+
}).sort();
|
|
3036
|
+
parts.push(`[${attributes.join(",")}]`);
|
|
3037
|
+
}
|
|
3038
|
+
for (const child of current.children ?? []) {
|
|
3039
|
+
if (isJsxNode(child)) {
|
|
3040
|
+
visit(child);
|
|
3041
|
+
} else if (isJsxExpressionContainer(child)) {
|
|
3042
|
+
walkJsx(child, visit);
|
|
3043
|
+
parts.push("{}");
|
|
3044
|
+
} else {
|
|
3045
|
+
parts.push("{}");
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
};
|
|
3049
|
+
visit(node);
|
|
3050
|
+
return parts.join("/");
|
|
3051
|
+
}
|
|
3052
|
+
function isSubstantial(node) {
|
|
3053
|
+
let count = 0;
|
|
3054
|
+
walkJsx(node, () => {
|
|
3055
|
+
count += 1;
|
|
3056
|
+
});
|
|
3057
|
+
return count >= 3;
|
|
3058
|
+
}
|
|
3059
|
+
function blockChildren(node) {
|
|
3060
|
+
return (node.children ?? []).filter(isJsxNode);
|
|
3061
|
+
}
|
|
3062
|
+
var repeatedStructureRule = {
|
|
2719
3063
|
meta: {
|
|
2720
3064
|
schema: [],
|
|
2721
3065
|
type: "problem",
|
|
2722
3066
|
docs: {
|
|
2723
|
-
description: "
|
|
3067
|
+
description: "Require extracting a repeated block of elements as a named component."
|
|
2724
3068
|
}
|
|
2725
3069
|
},
|
|
2726
3070
|
create(context) {
|
|
3071
|
+
if (!context.filename.endsWith(".tsx") && !context.filename.endsWith(".jsx")) return {};
|
|
2727
3072
|
return {
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
loc: comment.loc,
|
|
2734
|
-
message: "Do not use eslint-disable directives; fix the reported violation instead."
|
|
2735
|
-
});
|
|
2736
|
-
}
|
|
3073
|
+
JSXElement(node) {
|
|
3074
|
+
checkChildren(node);
|
|
3075
|
+
},
|
|
3076
|
+
JSXFragment(node) {
|
|
3077
|
+
checkChildren(node);
|
|
2737
3078
|
}
|
|
2738
3079
|
};
|
|
3080
|
+
function checkChildren(node) {
|
|
3081
|
+
const children = blockChildren(node);
|
|
3082
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3083
|
+
for (const child of children) {
|
|
3084
|
+
const sig = signature(child);
|
|
3085
|
+
const group = seen.get(sig) ?? [];
|
|
3086
|
+
group.push(child);
|
|
3087
|
+
seen.set(sig, group);
|
|
3088
|
+
}
|
|
3089
|
+
for (const [, group] of seen) {
|
|
3090
|
+
if (group.length < 2) continue;
|
|
3091
|
+
const first = group[0];
|
|
3092
|
+
if (!first || !isSubstantial(first)) continue;
|
|
3093
|
+
context.report({
|
|
3094
|
+
node: first,
|
|
3095
|
+
message: `The same arrangement of elements appears ${String(group.length)} times here; extract it as a named component. Different data or labels do not prevent extraction. See docs/code-organization-guide/rules/repeated-structure-rule.md`
|
|
3096
|
+
});
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
2739
3099
|
}
|
|
2740
3100
|
};
|
|
2741
3101
|
|
|
@@ -2813,7 +3173,7 @@ var zodSchemaValidationRule = {
|
|
|
2813
3173
|
};
|
|
2814
3174
|
|
|
2815
3175
|
// eslint/pasika/rules/source-under-src.ts
|
|
2816
|
-
import
|
|
3176
|
+
import path32 from "path";
|
|
2817
3177
|
var NON_SOURCE_ROOT_DIRS = /* @__PURE__ */ new Set([
|
|
2818
3178
|
".agents",
|
|
2819
3179
|
".cache",
|
|
@@ -2854,14 +3214,14 @@ var sourceUnderSrcRule = {
|
|
|
2854
3214
|
}
|
|
2855
3215
|
},
|
|
2856
3216
|
create(context) {
|
|
2857
|
-
const filename =
|
|
3217
|
+
const filename = path32.resolve(context.filename);
|
|
2858
3218
|
if (!MODULE_EXTENSION.test(filename)) return {};
|
|
2859
|
-
const relative =
|
|
3219
|
+
const relative = path32.relative(process.cwd(), filename).replace(/\\/g, "/");
|
|
2860
3220
|
if (relative === "src" || relative.startsWith("src/")) return {};
|
|
2861
3221
|
const topLevel = relative.split("/")[0] ?? "";
|
|
2862
3222
|
if (NON_SOURCE_ROOT_DIRS.has(topLevel)) return {};
|
|
2863
3223
|
if (!relative.includes("/")) {
|
|
2864
|
-
const basename =
|
|
3224
|
+
const basename = path32.basename(filename);
|
|
2865
3225
|
if (CONFIG_FILE.test(basename) || DECLARATION_FILE.test(basename) || basename.startsWith(".")) return {};
|
|
2866
3226
|
}
|
|
2867
3227
|
return {
|
|
@@ -2923,7 +3283,7 @@ var docKindSuffixRule = {
|
|
|
2923
3283
|
};
|
|
2924
3284
|
|
|
2925
3285
|
// eslint/pasika/rules/md/title-matches-file-name.ts
|
|
2926
|
-
import
|
|
3286
|
+
import path33 from "path";
|
|
2927
3287
|
function toExpectedFileName(title) {
|
|
2928
3288
|
return `${title.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}.md`;
|
|
2929
3289
|
}
|
|
@@ -2943,7 +3303,7 @@ var titleMatchesFileNameRule = {
|
|
|
2943
3303
|
if (!filename.endsWith(".md")) return;
|
|
2944
3304
|
const title = getTextContent(node).trim();
|
|
2945
3305
|
const expectedFileName = toExpectedFileName(title);
|
|
2946
|
-
const actualFileName =
|
|
3306
|
+
const actualFileName = path33.basename(filename);
|
|
2947
3307
|
if (!title) {
|
|
2948
3308
|
context.report({
|
|
2949
3309
|
node,
|
|
@@ -3416,7 +3776,7 @@ var referenceBlockHeadingsRule = {
|
|
|
3416
3776
|
};
|
|
3417
3777
|
|
|
3418
3778
|
// eslint/pasika/rules/md/support-document-placement.ts
|
|
3419
|
-
import
|
|
3779
|
+
import path34 from "path";
|
|
3420
3780
|
var supportDocumentPlacementRule = {
|
|
3421
3781
|
meta: {
|
|
3422
3782
|
type: "problem",
|
|
@@ -3430,7 +3790,7 @@ var supportDocumentPlacementRule = {
|
|
|
3430
3790
|
root(node) {
|
|
3431
3791
|
const filename = getFilename(context);
|
|
3432
3792
|
if (!filename.endsWith(".md")) return;
|
|
3433
|
-
const parentFolder =
|
|
3793
|
+
const parentFolder = path34.basename(path34.dirname(filename));
|
|
3434
3794
|
if (filename.endsWith("-rule.md") && parentFolder !== "rules") {
|
|
3435
3795
|
context.report({
|
|
3436
3796
|
node,
|
|
@@ -3473,11 +3833,11 @@ var noTemplatePromptRule = {
|
|
|
3473
3833
|
};
|
|
3474
3834
|
|
|
3475
3835
|
// eslint/pasika/rules/md/guide-folder-entry-point.ts
|
|
3476
|
-
import
|
|
3836
|
+
import path36 from "path";
|
|
3477
3837
|
|
|
3478
3838
|
// eslint/pasika/rules/md/project-index.ts
|
|
3479
3839
|
import { readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
|
|
3480
|
-
import
|
|
3840
|
+
import path35 from "path";
|
|
3481
3841
|
var KIND_BY_SUFFIX = [
|
|
3482
3842
|
["-rule.md", "rule"],
|
|
3483
3843
|
["-guide.md", "guide"],
|
|
@@ -3486,7 +3846,7 @@ var KIND_BY_SUFFIX = [
|
|
|
3486
3846
|
];
|
|
3487
3847
|
function listMarkdownFiles(dir) {
|
|
3488
3848
|
return readdirSync2(dir).flatMap((entry) => {
|
|
3489
|
-
const entryPath =
|
|
3849
|
+
const entryPath = path35.join(dir, entry);
|
|
3490
3850
|
if (statSync3(entryPath).isDirectory()) {
|
|
3491
3851
|
return entry.startsWith("_") ? [] : listMarkdownFiles(entryPath);
|
|
3492
3852
|
}
|
|
@@ -3504,11 +3864,11 @@ function getProjectDocs(docsRoot) {
|
|
|
3504
3864
|
if (cached) return cached;
|
|
3505
3865
|
const files = listMarkdownFiles(docsRoot);
|
|
3506
3866
|
const docs = files.sort((a, b) => a.localeCompare(b)).map((filePath) => {
|
|
3507
|
-
const fileName =
|
|
3867
|
+
const fileName = path35.basename(filePath);
|
|
3508
3868
|
const kind = KIND_BY_SUFFIX.find(([suffix]) => fileName.endsWith(suffix))?.[1];
|
|
3509
3869
|
return {
|
|
3510
3870
|
filePath,
|
|
3511
|
-
doc:
|
|
3871
|
+
doc: path35.relative(docsRoot, filePath).split(path35.sep).join("/"),
|
|
3512
3872
|
fileName,
|
|
3513
3873
|
kind,
|
|
3514
3874
|
title: extractTitle(filePath)
|
|
@@ -3518,12 +3878,12 @@ function getProjectDocs(docsRoot) {
|
|
|
3518
3878
|
return docs;
|
|
3519
3879
|
}
|
|
3520
3880
|
function findDocsRoot(filePath) {
|
|
3521
|
-
let dir =
|
|
3881
|
+
let dir = path35.dirname(filePath);
|
|
3522
3882
|
for (; ; ) {
|
|
3523
|
-
if (
|
|
3883
|
+
if (path35.basename(dir) === "docs" && statSync3(dir).isDirectory()) {
|
|
3524
3884
|
return dir;
|
|
3525
3885
|
}
|
|
3526
|
-
const parent =
|
|
3886
|
+
const parent = path35.dirname(dir);
|
|
3527
3887
|
if (parent === dir) return void 0;
|
|
3528
3888
|
dir = parent;
|
|
3529
3889
|
}
|
|
@@ -3547,13 +3907,13 @@ var guideFolderEntryPointRule = {
|
|
|
3547
3907
|
if (!docsRoot) return;
|
|
3548
3908
|
const docs = getProjectDocs(docsRoot);
|
|
3549
3909
|
const guideFolders = new Set(
|
|
3550
|
-
docs.filter((doc) => ["rules", "references"].includes(
|
|
3910
|
+
docs.filter((doc) => ["rules", "references"].includes(path36.basename(path36.dirname(doc.filePath)))).map((doc) => path36.dirname(path36.dirname(doc.filePath))).filter((folder) => path36.resolve(folder) !== path36.resolve(docsRoot))
|
|
3551
3911
|
);
|
|
3552
|
-
const currentDir =
|
|
3912
|
+
const currentDir = path36.dirname(filename);
|
|
3553
3913
|
if (guideFolders.has(currentDir)) {
|
|
3554
|
-
const expectedEntryPoint = `${
|
|
3914
|
+
const expectedEntryPoint = `${path36.basename(currentDir)}.md`;
|
|
3555
3915
|
const hasEntryPoint = docs.some(
|
|
3556
|
-
(doc) => doc.kind === "guide" &&
|
|
3916
|
+
(doc) => doc.kind === "guide" && path36.dirname(doc.filePath) === currentDir && doc.fileName === expectedEntryPoint
|
|
3557
3917
|
);
|
|
3558
3918
|
if (!hasEntryPoint) {
|
|
3559
3919
|
context.report({
|
|
@@ -3610,6 +3970,39 @@ var rfcOnlyInBulletsRule = {
|
|
|
3610
3970
|
}
|
|
3611
3971
|
};
|
|
3612
3972
|
|
|
3973
|
+
// eslint/pasika/rules/md/rfc-keyword-in-every-bullet.ts
|
|
3974
|
+
function checkBullets(node, context) {
|
|
3975
|
+
if (node.type === "listItem" && !containsRfcKeyword(getTextContent(node))) {
|
|
3976
|
+
context.report({
|
|
3977
|
+
node,
|
|
3978
|
+
message: "bullet point contains no RFC 2119 keyword"
|
|
3979
|
+
});
|
|
3980
|
+
}
|
|
3981
|
+
if ("children" in node) {
|
|
3982
|
+
for (const child of node.children) {
|
|
3983
|
+
checkBullets(child, context);
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
var rfcKeywordInEveryBulletRule = {
|
|
3988
|
+
meta: {
|
|
3989
|
+
type: "problem",
|
|
3990
|
+
docs: {
|
|
3991
|
+
description: "Every bullet in a rule or policy document must contain an RFC 2119 keyword.",
|
|
3992
|
+
recommended: true
|
|
3993
|
+
}
|
|
3994
|
+
},
|
|
3995
|
+
create(context) {
|
|
3996
|
+
return {
|
|
3997
|
+
root(node) {
|
|
3998
|
+
const filename = getFilename(context);
|
|
3999
|
+
if (!filename.endsWith("-rule.md") && !filename.endsWith("-policy.md")) return;
|
|
4000
|
+
checkBullets(node, context);
|
|
4001
|
+
}
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
};
|
|
4005
|
+
|
|
3613
4006
|
// eslint/pasika/rules/md/policy-subject-headings.ts
|
|
3614
4007
|
var policySubjectHeadingsRule = {
|
|
3615
4008
|
meta: {
|
|
@@ -3745,7 +4138,7 @@ var noNestedHowToRule = {
|
|
|
3745
4138
|
|
|
3746
4139
|
// eslint/pasika/rules/md/glossary-term-linking.ts
|
|
3747
4140
|
import { readFileSync as readFileSync5 } from "fs";
|
|
3748
|
-
import
|
|
4141
|
+
import path37 from "path";
|
|
3749
4142
|
function extractGlossaryTerms(filePath) {
|
|
3750
4143
|
const content = readFileSync5(filePath, "utf8");
|
|
3751
4144
|
const terms = [];
|
|
@@ -3799,9 +4192,9 @@ var glossaryTermLinkingRule = {
|
|
|
3799
4192
|
const docsRoot = findDocsRoot(filename);
|
|
3800
4193
|
if (!docsRoot) return;
|
|
3801
4194
|
const docs = getProjectDocs(docsRoot);
|
|
3802
|
-
const guideDir =
|
|
4195
|
+
const guideDir = path37.dirname(filename);
|
|
3803
4196
|
const guideReferences = docs.filter(
|
|
3804
|
-
(doc) => doc.kind === "reference" &&
|
|
4197
|
+
(doc) => doc.kind === "reference" && path37.dirname(doc.filePath) === guideDir
|
|
3805
4198
|
);
|
|
3806
4199
|
if (guideReferences.length === 0) return;
|
|
3807
4200
|
const glossaryTerms = [];
|
|
@@ -3845,6 +4238,7 @@ var mdRules = {
|
|
|
3845
4238
|
"no-template-prompt": noTemplatePromptRule,
|
|
3846
4239
|
"guide-folder-entry-point": guideFolderEntryPointRule,
|
|
3847
4240
|
"rfc-only-in-bullets": rfcOnlyInBulletsRule,
|
|
4241
|
+
"rfc-keyword-in-every-bullet": rfcKeywordInEveryBulletRule,
|
|
3848
4242
|
"policy-subject-headings": policySubjectHeadingsRule,
|
|
3849
4243
|
"guide-link-anchors": guideLinkAnchorsRule,
|
|
3850
4244
|
"no-nested-how-to": noNestedHowToRule,
|
|
@@ -3874,7 +4268,7 @@ function atrules(node) {
|
|
|
3874
4268
|
function atrulesNamed(node, name) {
|
|
3875
4269
|
return atrules(node).filter((candidate) => candidate.type === "Atrule" && candidate.name === name);
|
|
3876
4270
|
}
|
|
3877
|
-
function
|
|
4271
|
+
function blockChildren2(node) {
|
|
3878
4272
|
if (!node) return [];
|
|
3879
4273
|
if (!("block" in node) || !node.block) return [];
|
|
3880
4274
|
return node.block.children;
|
|
@@ -3921,7 +4315,7 @@ var themeResetRule = {
|
|
|
3921
4315
|
return {
|
|
3922
4316
|
"StyleSheet:exit"(node) {
|
|
3923
4317
|
const resetFound = atrulesNamed(node, "theme").some(
|
|
3924
|
-
(theme) =>
|
|
4318
|
+
(theme) => blockChildren2(theme).some((child) => {
|
|
3925
4319
|
if (child.type === "Declaration") return child.property.startsWith("--*");
|
|
3926
4320
|
if (child.type === "Raw") return child.value.includes("--*: initial");
|
|
3927
4321
|
return false;
|
|
@@ -3952,7 +4346,7 @@ var rootVariablesRule = {
|
|
|
3952
4346
|
"StyleSheet:exit"(node) {
|
|
3953
4347
|
const hasRootVars = rules(node).some((rule) => {
|
|
3954
4348
|
if (!selectorNames(rule).includes("root")) return false;
|
|
3955
|
-
return
|
|
4349
|
+
return blockChildren2(rule).some((child) => child.type === "Declaration");
|
|
3956
4350
|
});
|
|
3957
4351
|
if (!hasRootVars) {
|
|
3958
4352
|
context.report({
|
|
@@ -3961,7 +4355,7 @@ var rootVariablesRule = {
|
|
|
3961
4355
|
});
|
|
3962
4356
|
}
|
|
3963
4357
|
const referencingThemes = atrulesNamed(node, "theme").filter(
|
|
3964
|
-
(theme) =>
|
|
4358
|
+
(theme) => blockChildren2(theme).some((child) => {
|
|
3965
4359
|
if (child.type === "Raw") return child.value.includes("var(");
|
|
3966
4360
|
if (child.type === "Declaration") return JSON.stringify(child.value).includes("var(");
|
|
3967
4361
|
return false;
|
|
@@ -3995,9 +4389,9 @@ var applyUsageRule = {
|
|
|
3995
4389
|
"StyleSheet:exit"(node) {
|
|
3996
4390
|
const layers = node.children.filter((child) => child.type === "Atrule" && child.name === "layer");
|
|
3997
4391
|
for (const layer of layers) {
|
|
3998
|
-
for (const child of
|
|
4392
|
+
for (const child of blockChildren2(layer)) {
|
|
3999
4393
|
if (child.type !== "Rule") continue;
|
|
4000
|
-
for (const declaration of
|
|
4394
|
+
for (const declaration of blockChildren2(child)) {
|
|
4001
4395
|
if (declaration.type !== "Declaration") continue;
|
|
4002
4396
|
context.report({
|
|
4003
4397
|
node: declaration,
|
|
@@ -4027,10 +4421,10 @@ var baseLayerPairRule = {
|
|
|
4027
4421
|
if (baseLayers.length === 0) return;
|
|
4028
4422
|
const applied = [];
|
|
4029
4423
|
for (const layer of baseLayers) {
|
|
4030
|
-
for (const child of
|
|
4424
|
+
for (const child of blockChildren2(layer)) {
|
|
4031
4425
|
if (child.type !== "Rule") continue;
|
|
4032
4426
|
if (!selectorNames(child).includes("body")) continue;
|
|
4033
|
-
for (const declaration of
|
|
4427
|
+
for (const declaration of blockChildren2(child)) {
|
|
4034
4428
|
if (declaration.type !== "Atrule" || declaration.name !== "apply") continue;
|
|
4035
4429
|
applied.push(...preludeIdentifiers(declaration));
|
|
4036
4430
|
}
|
|
@@ -4155,7 +4549,7 @@ var customUtilityApplyRule = {
|
|
|
4155
4549
|
return {
|
|
4156
4550
|
"StyleSheet:exit"(node) {
|
|
4157
4551
|
for (const utility of atrulesNamed(node, "utility")) {
|
|
4158
|
-
for (const child of
|
|
4552
|
+
for (const child of blockChildren2(utility)) {
|
|
4159
4553
|
if (child.type === "Atrule" && child.name === "apply") continue;
|
|
4160
4554
|
if (child.type === "Declaration") {
|
|
4161
4555
|
context.report({
|
|
@@ -4227,7 +4621,7 @@ var themeVariableNamespaceRule = {
|
|
|
4227
4621
|
"StyleSheet:exit"(node) {
|
|
4228
4622
|
const namespaces = /* @__PURE__ */ new Set();
|
|
4229
4623
|
for (const theme of atrulesNamed(node, "theme")) {
|
|
4230
|
-
for (const child of
|
|
4624
|
+
for (const child of blockChildren2(theme)) {
|
|
4231
4625
|
if (child.type !== "Declaration") continue;
|
|
4232
4626
|
const segments = child.property.replace(/^--/, "").split("-");
|
|
4233
4627
|
if (segments[0]) namespaces.add(segments[0]);
|
|
@@ -4297,43 +4691,8 @@ var cssRules = {
|
|
|
4297
4691
|
"global-css-location": globalCssLocationRule
|
|
4298
4692
|
};
|
|
4299
4693
|
|
|
4300
|
-
// eslint/pasika/rules/json/no-cache-flag.ts
|
|
4301
|
-
function memberName(member) {
|
|
4302
|
-
return member.name.type === "String" ? member.name.value : member.name.name;
|
|
4303
|
-
}
|
|
4304
|
-
var noCacheFlagRule = {
|
|
4305
|
-
meta: {
|
|
4306
|
-
schema: [],
|
|
4307
|
-
type: "problem",
|
|
4308
|
-
docs: {
|
|
4309
|
-
description: "Require lint scripts to not pass ESLint's --cache flag."
|
|
4310
|
-
}
|
|
4311
|
-
},
|
|
4312
|
-
create(context) {
|
|
4313
|
-
return {
|
|
4314
|
-
Document(node) {
|
|
4315
|
-
const root = node.body;
|
|
4316
|
-
if (root.type !== "Object") return;
|
|
4317
|
-
const scripts = root.members.find((member) => memberName(member) === "scripts");
|
|
4318
|
-
if (scripts?.value.type !== "Object") return;
|
|
4319
|
-
for (const member of scripts.value.members) {
|
|
4320
|
-
const name = memberName(member);
|
|
4321
|
-
if (name !== "lint" && name !== "fix") continue;
|
|
4322
|
-
if (member.value.type !== "String") continue;
|
|
4323
|
-
if (member.value.value.includes("--cache")) {
|
|
4324
|
-
context.report({
|
|
4325
|
-
node: member,
|
|
4326
|
-
message: "Lint scripts must not pass ESLint's --cache flag because cross-file rules require every file in the run."
|
|
4327
|
-
});
|
|
4328
|
-
}
|
|
4329
|
-
}
|
|
4330
|
-
}
|
|
4331
|
-
};
|
|
4332
|
-
}
|
|
4333
|
-
};
|
|
4334
|
-
|
|
4335
4694
|
// eslint/pasika/rules/json/no-vulyk-dependency.ts
|
|
4336
|
-
function
|
|
4695
|
+
function memberName(member) {
|
|
4337
4696
|
return member.name.type === "String" ? member.name.value : member.name.name;
|
|
4338
4697
|
}
|
|
4339
4698
|
var noVulykDependencyRule = {
|
|
@@ -4350,9 +4709,9 @@ var noVulykDependencyRule = {
|
|
|
4350
4709
|
const root = node.body;
|
|
4351
4710
|
if (root.type !== "Object") return;
|
|
4352
4711
|
for (const key of ["dependencies", "devDependencies"]) {
|
|
4353
|
-
const section = root.members.find((member) =>
|
|
4712
|
+
const section = root.members.find((member) => memberName(member) === key);
|
|
4354
4713
|
if (section?.value.type !== "Object") continue;
|
|
4355
|
-
const vulyk = section.value.members.find((member) =>
|
|
4714
|
+
const vulyk = section.value.members.find((member) => memberName(member) === "vulyk");
|
|
4356
4715
|
if (vulyk) {
|
|
4357
4716
|
context.report({
|
|
4358
4717
|
node: vulyk,
|
|
@@ -4367,7 +4726,6 @@ var noVulykDependencyRule = {
|
|
|
4367
4726
|
|
|
4368
4727
|
// eslint/pasika/rules/json/index.ts
|
|
4369
4728
|
var jsonRules = {
|
|
4370
|
-
"no-cache-flag": noCacheFlagRule,
|
|
4371
4729
|
"no-vulyk-dependency": noVulykDependencyRule
|
|
4372
4730
|
};
|
|
4373
4731
|
|
|
@@ -4406,8 +4764,10 @@ var pasikaRules = {
|
|
|
4406
4764
|
"stay-flat": stayFlatRule,
|
|
4407
4765
|
"type-extraction": typeExtractionRule,
|
|
4408
4766
|
"locale-placement": localePlacementRule,
|
|
4767
|
+
"sole-state-owner": soleStateOwnerRule,
|
|
4768
|
+
"locale-key-shape": localeKeyShapeRule,
|
|
4409
4769
|
"shared-style-dedup": sharedStyleDedupRule,
|
|
4410
|
-
"
|
|
4770
|
+
"repeated-structure": repeatedStructureRule,
|
|
4411
4771
|
"zod-schema-validation": zodSchemaValidationRule,
|
|
4412
4772
|
"source-under-src": sourceUnderSrcRule
|
|
4413
4773
|
};
|
|
@@ -4435,7 +4795,7 @@ function hashRequirement(canonicalText) {
|
|
|
4435
4795
|
|
|
4436
4796
|
// enforcement/parse-docs.ts
|
|
4437
4797
|
import { readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
|
|
4438
|
-
import
|
|
4798
|
+
import path38 from "path";
|
|
4439
4799
|
var KIND_BY_SUFFIX2 = [
|
|
4440
4800
|
["-rule.md", "rule"],
|
|
4441
4801
|
["-guide.md", "guide"],
|
|
@@ -4445,7 +4805,7 @@ var KIND_BY_SUFFIX2 = [
|
|
|
4445
4805
|
var RFC_2119 = /\b(?<keyword>MUST NOT|MUST|SHOULD NOT|SHOULD|MAY)\b/;
|
|
4446
4806
|
function listMarkdownFiles2(dir) {
|
|
4447
4807
|
return readdirSync3(dir).flatMap((entry) => {
|
|
4448
|
-
const entryPath =
|
|
4808
|
+
const entryPath = path38.join(dir, entry);
|
|
4449
4809
|
if (statSync4(entryPath).isDirectory()) {
|
|
4450
4810
|
return entry.startsWith("_") ? [] : listMarkdownFiles2(entryPath);
|
|
4451
4811
|
}
|
|
@@ -4471,7 +4831,7 @@ function parseDoc(filePath, docsRoot) {
|
|
|
4471
4831
|
const sourceLines = body.split("\n");
|
|
4472
4832
|
const prose = toProse(body, "$<content>");
|
|
4473
4833
|
const proseWithoutCode = toProse(body, "");
|
|
4474
|
-
const fileName =
|
|
4834
|
+
const fileName = path38.basename(filePath);
|
|
4475
4835
|
const kind = KIND_BY_SUFFIX2.find(([suffix]) => fileName.endsWith(suffix))?.[1];
|
|
4476
4836
|
const requirements = [];
|
|
4477
4837
|
const steps = [];
|
|
@@ -4501,7 +4861,7 @@ function parseDoc(filePath, docsRoot) {
|
|
|
4501
4861
|
const overview = prose.slice(1).find((line) => line.trim() !== "" && !line.startsWith("#"));
|
|
4502
4862
|
return {
|
|
4503
4863
|
filePath,
|
|
4504
|
-
doc:
|
|
4864
|
+
doc: path38.relative(docsRoot, filePath).split(path38.sep).join("/"),
|
|
4505
4865
|
fileName,
|
|
4506
4866
|
kind,
|
|
4507
4867
|
title,
|
|
@@ -4521,7 +4881,7 @@ function parseDocs(docsRoot) {
|
|
|
4521
4881
|
|
|
4522
4882
|
// enforcement/doctor.ts
|
|
4523
4883
|
import fs4 from "fs";
|
|
4524
|
-
import
|
|
4884
|
+
import path39 from "path";
|
|
4525
4885
|
import { z } from "zod";
|
|
4526
4886
|
var doctorCheckRefs = [
|
|
4527
4887
|
"pasika/pasika-installed",
|
|
@@ -4565,7 +4925,7 @@ function checkFrameworkPackages(pkg) {
|
|
|
4565
4925
|
}
|
|
4566
4926
|
function checkSourceRoot(cwd) {
|
|
4567
4927
|
const findings = [];
|
|
4568
|
-
const srcDir =
|
|
4928
|
+
const srcDir = path39.join(cwd, "src");
|
|
4569
4929
|
if (!fs4.existsSync(srcDir)) {
|
|
4570
4930
|
findings.push({
|
|
4571
4931
|
check: "source-under-src",
|
|
@@ -4591,14 +4951,14 @@ function checkSourceRoot(cwd) {
|
|
|
4591
4951
|
function findGlobalStylesheet(cwd) {
|
|
4592
4952
|
const candidates = ["src/app/globals.css", "src/styles/globals.css", "src/globals.css"];
|
|
4593
4953
|
for (const candidate of candidates) {
|
|
4594
|
-
const full =
|
|
4954
|
+
const full = path39.join(cwd, candidate);
|
|
4595
4955
|
if (fs4.existsSync(full)) return full;
|
|
4596
4956
|
}
|
|
4597
|
-
const srcDir =
|
|
4957
|
+
const srcDir = path39.join(cwd, "src");
|
|
4598
4958
|
if (!fs4.existsSync(srcDir)) return void 0;
|
|
4599
4959
|
const walk = (dir) => {
|
|
4600
4960
|
for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
|
|
4601
|
-
const full =
|
|
4961
|
+
const full = path39.join(dir, entry.name);
|
|
4602
4962
|
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".next") {
|
|
4603
4963
|
const found = walk(full);
|
|
4604
4964
|
if (found) return found;
|
|
@@ -4629,7 +4989,7 @@ function checkGlobalStylesheet(cwd) {
|
|
|
4629
4989
|
function checkConfigBaseline(cwd) {
|
|
4630
4990
|
const findings = [];
|
|
4631
4991
|
const eslintConfigs = ["eslint.config.ts", "eslint.config.mjs", "eslint.config.js"];
|
|
4632
|
-
const eslintConfig = eslintConfigs.find((name) => fs4.existsSync(
|
|
4992
|
+
const eslintConfig = eslintConfigs.find((name) => fs4.existsSync(path39.join(cwd, name)));
|
|
4633
4993
|
if (!eslintConfig) {
|
|
4634
4994
|
findings.push({
|
|
4635
4995
|
check: "config-baseline",
|
|
@@ -4637,7 +4997,7 @@ function checkConfigBaseline(cwd) {
|
|
|
4637
4997
|
severity: "error"
|
|
4638
4998
|
});
|
|
4639
4999
|
} else {
|
|
4640
|
-
const content = fs4.readFileSync(
|
|
5000
|
+
const content = fs4.readFileSync(path39.join(cwd, eslintConfig), "utf8");
|
|
4641
5001
|
if (!content.includes("zirka")) {
|
|
4642
5002
|
findings.push({
|
|
4643
5003
|
check: "config-baseline",
|
|
@@ -4646,7 +5006,7 @@ function checkConfigBaseline(cwd) {
|
|
|
4646
5006
|
});
|
|
4647
5007
|
}
|
|
4648
5008
|
}
|
|
4649
|
-
if (!fs4.existsSync(
|
|
5009
|
+
if (!fs4.existsSync(path39.join(cwd, "tsconfig.json"))) {
|
|
4650
5010
|
findings.push({
|
|
4651
5011
|
check: "config-baseline",
|
|
4652
5012
|
message: "No tsconfig.json found. Create one extending the pasika baseline.",
|
|
@@ -4657,31 +5017,29 @@ function checkConfigBaseline(cwd) {
|
|
|
4657
5017
|
}
|
|
4658
5018
|
function checkManagedFiles(cwd) {
|
|
4659
5019
|
const findings = [];
|
|
4660
|
-
const
|
|
4661
|
-
if (!fs4.existsSync(
|
|
4662
|
-
const
|
|
4663
|
-
if (!fs4.existsSync(
|
|
5020
|
+
const configPath = path39.join(cwd, "vulyk.config.ts");
|
|
5021
|
+
if (!fs4.existsSync(configPath)) return findings;
|
|
5022
|
+
const lockPath = path39.join(cwd, "vulyk.lock.json");
|
|
5023
|
+
if (!fs4.existsSync(lockPath)) return findings;
|
|
4664
5024
|
try {
|
|
4665
|
-
const
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
const
|
|
4669
|
-
const
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
const
|
|
4673
|
-
for (const
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
});
|
|
4684
|
-
}
|
|
5025
|
+
const configMtime = fs4.statSync(configPath).mtimeMs;
|
|
5026
|
+
const lockMtime = fs4.statSync(lockPath).mtimeMs;
|
|
5027
|
+
const referenceMtime = Math.max(configMtime, lockMtime);
|
|
5028
|
+
const defaultOutputDirs = ["docs/external"];
|
|
5029
|
+
for (const dir of defaultOutputDirs) {
|
|
5030
|
+
const dirPath = path39.join(cwd, dir);
|
|
5031
|
+
if (!fs4.existsSync(dirPath)) continue;
|
|
5032
|
+
const entries = fs4.readdirSync(dirPath, { withFileTypes: true });
|
|
5033
|
+
for (const entry of entries) {
|
|
5034
|
+
if (!entry.isFile()) continue;
|
|
5035
|
+
const filePath = path39.join(dirPath, entry.name);
|
|
5036
|
+
const stat = fs4.statSync(filePath);
|
|
5037
|
+
if (stat.mtimeMs > referenceMtime + 1e3) {
|
|
5038
|
+
findings.push({
|
|
5039
|
+
check: "managed-file-edit",
|
|
5040
|
+
message: `File "${dir}/${entry.name}" appears to have been edited after vulyk wrote it. Use vulyk commands to update managed files.`,
|
|
5041
|
+
severity: "warning"
|
|
5042
|
+
});
|
|
4685
5043
|
}
|
|
4686
5044
|
}
|
|
4687
5045
|
}
|
|
@@ -4690,7 +5048,7 @@ function checkManagedFiles(cwd) {
|
|
|
4690
5048
|
return findings;
|
|
4691
5049
|
}
|
|
4692
5050
|
function runDoctor(cwd) {
|
|
4693
|
-
const pkgPath =
|
|
5051
|
+
const pkgPath = path39.join(cwd, "package.json");
|
|
4694
5052
|
const pkg = readPackageJson(pkgPath);
|
|
4695
5053
|
return [
|
|
4696
5054
|
...checkFrameworkPackages(pkg),
|
|
@@ -4738,7 +5096,7 @@ function collectTestTitles(rulesDir) {
|
|
|
4738
5096
|
const titles = /* @__PURE__ */ new Set();
|
|
4739
5097
|
const visit = (dir) => {
|
|
4740
5098
|
for (const entry of readdirSync4(dir)) {
|
|
4741
|
-
const entryPath =
|
|
5099
|
+
const entryPath = path40.join(dir, entry);
|
|
4742
5100
|
if (statSync5(entryPath).isDirectory()) {
|
|
4743
5101
|
visit(entryPath);
|
|
4744
5102
|
continue;
|
|
@@ -4748,7 +5106,7 @@ function collectTestTitles(rulesDir) {
|
|
|
4748
5106
|
const titlePattern = /\b(?:describe|test|it)\(\s*(?:"(?<double>(?:[^"\\]|\\.)*)"|'(?<single>(?:[^'\\]|\\.)*)')/g;
|
|
4749
5107
|
for (const match of body.matchAll(titlePattern)) {
|
|
4750
5108
|
const title = match.groups?.double ?? match.groups?.single ?? "";
|
|
4751
|
-
titles.add(title.replaceAll('\\\\"', '"').replaceAll("\\\\'", "'"));
|
|
5109
|
+
titles.add(title.replaceAll("`", "").replaceAll('\\\\"', '"').replaceAll("\\\\'", "'"));
|
|
4752
5110
|
}
|
|
4753
5111
|
}
|
|
4754
5112
|
};
|
|
@@ -4921,12 +5279,12 @@ function json(data) {
|
|
|
4921
5279
|
}
|
|
4922
5280
|
|
|
4923
5281
|
// cli/index.ts
|
|
4924
|
-
var REGISTRY_RELATIVE_PATH =
|
|
5282
|
+
var REGISTRY_RELATIVE_PATH = path41.join("enforcement", "registry.json");
|
|
4925
5283
|
function findRegistryRoot(startDir) {
|
|
4926
|
-
let dir =
|
|
5284
|
+
let dir = path41.resolve(startDir);
|
|
4927
5285
|
for (; ; ) {
|
|
4928
|
-
if (existsSync(
|
|
4929
|
-
const parent =
|
|
5286
|
+
if (existsSync(path41.join(dir, REGISTRY_RELATIVE_PATH))) return dir;
|
|
5287
|
+
const parent = path41.dirname(dir);
|
|
4930
5288
|
if (parent === dir) return void 0;
|
|
4931
5289
|
dir = parent;
|
|
4932
5290
|
}
|
|
@@ -4950,12 +5308,12 @@ program.command("coverage").description("Check that every documented requirement
|
|
|
4950
5308
|
error(`No ${REGISTRY_RELATIVE_PATH} found in this directory or any parent.`);
|
|
4951
5309
|
process.exit(1);
|
|
4952
5310
|
}
|
|
4953
|
-
const docsRoot =
|
|
5311
|
+
const docsRoot = path41.join(root, "docs");
|
|
4954
5312
|
if (!existsSync(docsRoot)) {
|
|
4955
5313
|
error(`No documentation folder at ${docsRoot}. Run coverage inside the pasika repository.`);
|
|
4956
5314
|
process.exit(1);
|
|
4957
5315
|
}
|
|
4958
|
-
const registryPath =
|
|
5316
|
+
const registryPath = path41.join(root, REGISTRY_RELATIVE_PATH);
|
|
4959
5317
|
if (options.classify !== void 0) {
|
|
4960
5318
|
try {
|
|
4961
5319
|
const result = classifyRequirement({
|
|
@@ -4974,7 +5332,7 @@ program.command("coverage").description("Check that every documented requirement
|
|
|
4974
5332
|
const report3 = buildCoverageReport({
|
|
4975
5333
|
docsRoot,
|
|
4976
5334
|
registry: readRegistry(registryPath),
|
|
4977
|
-
rulesDir:
|
|
5335
|
+
rulesDir: path41.join(root, "eslint", "pasika", "rules")
|
|
4978
5336
|
});
|
|
4979
5337
|
if (options.json) {
|
|
4980
5338
|
json(report3);
|