pasika 0.3.4 → 0.3.5
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 +507 -85
- package/dist/eslint/pasika/index.d.ts +4 -0
- package/dist/eslint/pasika/index.js +481 -59
- package/package.json +1 -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,6 +2991,114 @@ var sharedStyleDedupRule = {
|
|
|
2714
2991
|
}
|
|
2715
2992
|
};
|
|
2716
2993
|
|
|
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 = {
|
|
3063
|
+
meta: {
|
|
3064
|
+
schema: [],
|
|
3065
|
+
type: "problem",
|
|
3066
|
+
docs: {
|
|
3067
|
+
description: "Require extracting a repeated block of elements as a named component."
|
|
3068
|
+
}
|
|
3069
|
+
},
|
|
3070
|
+
create(context) {
|
|
3071
|
+
if (!context.filename.endsWith(".tsx") && !context.filename.endsWith(".jsx")) return {};
|
|
3072
|
+
return {
|
|
3073
|
+
JSXElement(node) {
|
|
3074
|
+
checkChildren(node);
|
|
3075
|
+
},
|
|
3076
|
+
JSXFragment(node) {
|
|
3077
|
+
checkChildren(node);
|
|
3078
|
+
}
|
|
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
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
};
|
|
3101
|
+
|
|
2717
3102
|
// eslint/pasika/rules/no-eslint-disable.ts
|
|
2718
3103
|
var noEslintDisableRule = {
|
|
2719
3104
|
meta: {
|
|
@@ -2813,7 +3198,7 @@ var zodSchemaValidationRule = {
|
|
|
2813
3198
|
};
|
|
2814
3199
|
|
|
2815
3200
|
// eslint/pasika/rules/source-under-src.ts
|
|
2816
|
-
import
|
|
3201
|
+
import path32 from "path";
|
|
2817
3202
|
var NON_SOURCE_ROOT_DIRS = /* @__PURE__ */ new Set([
|
|
2818
3203
|
".agents",
|
|
2819
3204
|
".cache",
|
|
@@ -2854,14 +3239,14 @@ var sourceUnderSrcRule = {
|
|
|
2854
3239
|
}
|
|
2855
3240
|
},
|
|
2856
3241
|
create(context) {
|
|
2857
|
-
const filename =
|
|
3242
|
+
const filename = path32.resolve(context.filename);
|
|
2858
3243
|
if (!MODULE_EXTENSION.test(filename)) return {};
|
|
2859
|
-
const relative =
|
|
3244
|
+
const relative = path32.relative(process.cwd(), filename).replace(/\\/g, "/");
|
|
2860
3245
|
if (relative === "src" || relative.startsWith("src/")) return {};
|
|
2861
3246
|
const topLevel = relative.split("/")[0] ?? "";
|
|
2862
3247
|
if (NON_SOURCE_ROOT_DIRS.has(topLevel)) return {};
|
|
2863
3248
|
if (!relative.includes("/")) {
|
|
2864
|
-
const basename =
|
|
3249
|
+
const basename = path32.basename(filename);
|
|
2865
3250
|
if (CONFIG_FILE.test(basename) || DECLARATION_FILE.test(basename) || basename.startsWith(".")) return {};
|
|
2866
3251
|
}
|
|
2867
3252
|
return {
|
|
@@ -2923,7 +3308,7 @@ var docKindSuffixRule = {
|
|
|
2923
3308
|
};
|
|
2924
3309
|
|
|
2925
3310
|
// eslint/pasika/rules/md/title-matches-file-name.ts
|
|
2926
|
-
import
|
|
3311
|
+
import path33 from "path";
|
|
2927
3312
|
function toExpectedFileName(title) {
|
|
2928
3313
|
return `${title.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}.md`;
|
|
2929
3314
|
}
|
|
@@ -2943,7 +3328,7 @@ var titleMatchesFileNameRule = {
|
|
|
2943
3328
|
if (!filename.endsWith(".md")) return;
|
|
2944
3329
|
const title = getTextContent(node).trim();
|
|
2945
3330
|
const expectedFileName = toExpectedFileName(title);
|
|
2946
|
-
const actualFileName =
|
|
3331
|
+
const actualFileName = path33.basename(filename);
|
|
2947
3332
|
if (!title) {
|
|
2948
3333
|
context.report({
|
|
2949
3334
|
node,
|
|
@@ -3416,7 +3801,7 @@ var referenceBlockHeadingsRule = {
|
|
|
3416
3801
|
};
|
|
3417
3802
|
|
|
3418
3803
|
// eslint/pasika/rules/md/support-document-placement.ts
|
|
3419
|
-
import
|
|
3804
|
+
import path34 from "path";
|
|
3420
3805
|
var supportDocumentPlacementRule = {
|
|
3421
3806
|
meta: {
|
|
3422
3807
|
type: "problem",
|
|
@@ -3430,7 +3815,7 @@ var supportDocumentPlacementRule = {
|
|
|
3430
3815
|
root(node) {
|
|
3431
3816
|
const filename = getFilename(context);
|
|
3432
3817
|
if (!filename.endsWith(".md")) return;
|
|
3433
|
-
const parentFolder =
|
|
3818
|
+
const parentFolder = path34.basename(path34.dirname(filename));
|
|
3434
3819
|
if (filename.endsWith("-rule.md") && parentFolder !== "rules") {
|
|
3435
3820
|
context.report({
|
|
3436
3821
|
node,
|
|
@@ -3473,11 +3858,11 @@ var noTemplatePromptRule = {
|
|
|
3473
3858
|
};
|
|
3474
3859
|
|
|
3475
3860
|
// eslint/pasika/rules/md/guide-folder-entry-point.ts
|
|
3476
|
-
import
|
|
3861
|
+
import path36 from "path";
|
|
3477
3862
|
|
|
3478
3863
|
// eslint/pasika/rules/md/project-index.ts
|
|
3479
3864
|
import { readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
|
|
3480
|
-
import
|
|
3865
|
+
import path35 from "path";
|
|
3481
3866
|
var KIND_BY_SUFFIX = [
|
|
3482
3867
|
["-rule.md", "rule"],
|
|
3483
3868
|
["-guide.md", "guide"],
|
|
@@ -3486,7 +3871,7 @@ var KIND_BY_SUFFIX = [
|
|
|
3486
3871
|
];
|
|
3487
3872
|
function listMarkdownFiles(dir) {
|
|
3488
3873
|
return readdirSync2(dir).flatMap((entry) => {
|
|
3489
|
-
const entryPath =
|
|
3874
|
+
const entryPath = path35.join(dir, entry);
|
|
3490
3875
|
if (statSync3(entryPath).isDirectory()) {
|
|
3491
3876
|
return entry.startsWith("_") ? [] : listMarkdownFiles(entryPath);
|
|
3492
3877
|
}
|
|
@@ -3504,11 +3889,11 @@ function getProjectDocs(docsRoot) {
|
|
|
3504
3889
|
if (cached) return cached;
|
|
3505
3890
|
const files = listMarkdownFiles(docsRoot);
|
|
3506
3891
|
const docs = files.sort((a, b) => a.localeCompare(b)).map((filePath) => {
|
|
3507
|
-
const fileName =
|
|
3892
|
+
const fileName = path35.basename(filePath);
|
|
3508
3893
|
const kind = KIND_BY_SUFFIX.find(([suffix]) => fileName.endsWith(suffix))?.[1];
|
|
3509
3894
|
return {
|
|
3510
3895
|
filePath,
|
|
3511
|
-
doc:
|
|
3896
|
+
doc: path35.relative(docsRoot, filePath).split(path35.sep).join("/"),
|
|
3512
3897
|
fileName,
|
|
3513
3898
|
kind,
|
|
3514
3899
|
title: extractTitle(filePath)
|
|
@@ -3518,12 +3903,12 @@ function getProjectDocs(docsRoot) {
|
|
|
3518
3903
|
return docs;
|
|
3519
3904
|
}
|
|
3520
3905
|
function findDocsRoot(filePath) {
|
|
3521
|
-
let dir =
|
|
3906
|
+
let dir = path35.dirname(filePath);
|
|
3522
3907
|
for (; ; ) {
|
|
3523
|
-
if (
|
|
3908
|
+
if (path35.basename(dir) === "docs" && statSync3(dir).isDirectory()) {
|
|
3524
3909
|
return dir;
|
|
3525
3910
|
}
|
|
3526
|
-
const parent =
|
|
3911
|
+
const parent = path35.dirname(dir);
|
|
3527
3912
|
if (parent === dir) return void 0;
|
|
3528
3913
|
dir = parent;
|
|
3529
3914
|
}
|
|
@@ -3547,13 +3932,13 @@ var guideFolderEntryPointRule = {
|
|
|
3547
3932
|
if (!docsRoot) return;
|
|
3548
3933
|
const docs = getProjectDocs(docsRoot);
|
|
3549
3934
|
const guideFolders = new Set(
|
|
3550
|
-
docs.filter((doc) => ["rules", "references"].includes(
|
|
3935
|
+
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
3936
|
);
|
|
3552
|
-
const currentDir =
|
|
3937
|
+
const currentDir = path36.dirname(filename);
|
|
3553
3938
|
if (guideFolders.has(currentDir)) {
|
|
3554
|
-
const expectedEntryPoint = `${
|
|
3939
|
+
const expectedEntryPoint = `${path36.basename(currentDir)}.md`;
|
|
3555
3940
|
const hasEntryPoint = docs.some(
|
|
3556
|
-
(doc) => doc.kind === "guide" &&
|
|
3941
|
+
(doc) => doc.kind === "guide" && path36.dirname(doc.filePath) === currentDir && doc.fileName === expectedEntryPoint
|
|
3557
3942
|
);
|
|
3558
3943
|
if (!hasEntryPoint) {
|
|
3559
3944
|
context.report({
|
|
@@ -3610,6 +3995,39 @@ var rfcOnlyInBulletsRule = {
|
|
|
3610
3995
|
}
|
|
3611
3996
|
};
|
|
3612
3997
|
|
|
3998
|
+
// eslint/pasika/rules/md/rfc-keyword-in-every-bullet.ts
|
|
3999
|
+
function checkBullets(node, context) {
|
|
4000
|
+
if (node.type === "listItem" && !containsRfcKeyword(getTextContent(node))) {
|
|
4001
|
+
context.report({
|
|
4002
|
+
node,
|
|
4003
|
+
message: "bullet point contains no RFC 2119 keyword"
|
|
4004
|
+
});
|
|
4005
|
+
}
|
|
4006
|
+
if ("children" in node) {
|
|
4007
|
+
for (const child of node.children) {
|
|
4008
|
+
checkBullets(child, context);
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
var rfcKeywordInEveryBulletRule = {
|
|
4013
|
+
meta: {
|
|
4014
|
+
type: "problem",
|
|
4015
|
+
docs: {
|
|
4016
|
+
description: "Every bullet in a rule or policy document must contain an RFC 2119 keyword.",
|
|
4017
|
+
recommended: true
|
|
4018
|
+
}
|
|
4019
|
+
},
|
|
4020
|
+
create(context) {
|
|
4021
|
+
return {
|
|
4022
|
+
root(node) {
|
|
4023
|
+
const filename = getFilename(context);
|
|
4024
|
+
if (!filename.endsWith("-rule.md") && !filename.endsWith("-policy.md")) return;
|
|
4025
|
+
checkBullets(node, context);
|
|
4026
|
+
}
|
|
4027
|
+
};
|
|
4028
|
+
}
|
|
4029
|
+
};
|
|
4030
|
+
|
|
3613
4031
|
// eslint/pasika/rules/md/policy-subject-headings.ts
|
|
3614
4032
|
var policySubjectHeadingsRule = {
|
|
3615
4033
|
meta: {
|
|
@@ -3745,7 +4163,7 @@ var noNestedHowToRule = {
|
|
|
3745
4163
|
|
|
3746
4164
|
// eslint/pasika/rules/md/glossary-term-linking.ts
|
|
3747
4165
|
import { readFileSync as readFileSync5 } from "fs";
|
|
3748
|
-
import
|
|
4166
|
+
import path37 from "path";
|
|
3749
4167
|
function extractGlossaryTerms(filePath) {
|
|
3750
4168
|
const content = readFileSync5(filePath, "utf8");
|
|
3751
4169
|
const terms = [];
|
|
@@ -3799,9 +4217,9 @@ var glossaryTermLinkingRule = {
|
|
|
3799
4217
|
const docsRoot = findDocsRoot(filename);
|
|
3800
4218
|
if (!docsRoot) return;
|
|
3801
4219
|
const docs = getProjectDocs(docsRoot);
|
|
3802
|
-
const guideDir =
|
|
4220
|
+
const guideDir = path37.dirname(filename);
|
|
3803
4221
|
const guideReferences = docs.filter(
|
|
3804
|
-
(doc) => doc.kind === "reference" &&
|
|
4222
|
+
(doc) => doc.kind === "reference" && path37.dirname(doc.filePath) === guideDir
|
|
3805
4223
|
);
|
|
3806
4224
|
if (guideReferences.length === 0) return;
|
|
3807
4225
|
const glossaryTerms = [];
|
|
@@ -3845,6 +4263,7 @@ var mdRules = {
|
|
|
3845
4263
|
"no-template-prompt": noTemplatePromptRule,
|
|
3846
4264
|
"guide-folder-entry-point": guideFolderEntryPointRule,
|
|
3847
4265
|
"rfc-only-in-bullets": rfcOnlyInBulletsRule,
|
|
4266
|
+
"rfc-keyword-in-every-bullet": rfcKeywordInEveryBulletRule,
|
|
3848
4267
|
"policy-subject-headings": policySubjectHeadingsRule,
|
|
3849
4268
|
"guide-link-anchors": guideLinkAnchorsRule,
|
|
3850
4269
|
"no-nested-how-to": noNestedHowToRule,
|
|
@@ -3874,7 +4293,7 @@ function atrules(node) {
|
|
|
3874
4293
|
function atrulesNamed(node, name) {
|
|
3875
4294
|
return atrules(node).filter((candidate) => candidate.type === "Atrule" && candidate.name === name);
|
|
3876
4295
|
}
|
|
3877
|
-
function
|
|
4296
|
+
function blockChildren2(node) {
|
|
3878
4297
|
if (!node) return [];
|
|
3879
4298
|
if (!("block" in node) || !node.block) return [];
|
|
3880
4299
|
return node.block.children;
|
|
@@ -3921,7 +4340,7 @@ var themeResetRule = {
|
|
|
3921
4340
|
return {
|
|
3922
4341
|
"StyleSheet:exit"(node) {
|
|
3923
4342
|
const resetFound = atrulesNamed(node, "theme").some(
|
|
3924
|
-
(theme) =>
|
|
4343
|
+
(theme) => blockChildren2(theme).some((child) => {
|
|
3925
4344
|
if (child.type === "Declaration") return child.property.startsWith("--*");
|
|
3926
4345
|
if (child.type === "Raw") return child.value.includes("--*: initial");
|
|
3927
4346
|
return false;
|
|
@@ -3952,7 +4371,7 @@ var rootVariablesRule = {
|
|
|
3952
4371
|
"StyleSheet:exit"(node) {
|
|
3953
4372
|
const hasRootVars = rules(node).some((rule) => {
|
|
3954
4373
|
if (!selectorNames(rule).includes("root")) return false;
|
|
3955
|
-
return
|
|
4374
|
+
return blockChildren2(rule).some((child) => child.type === "Declaration");
|
|
3956
4375
|
});
|
|
3957
4376
|
if (!hasRootVars) {
|
|
3958
4377
|
context.report({
|
|
@@ -3961,7 +4380,7 @@ var rootVariablesRule = {
|
|
|
3961
4380
|
});
|
|
3962
4381
|
}
|
|
3963
4382
|
const referencingThemes = atrulesNamed(node, "theme").filter(
|
|
3964
|
-
(theme) =>
|
|
4383
|
+
(theme) => blockChildren2(theme).some((child) => {
|
|
3965
4384
|
if (child.type === "Raw") return child.value.includes("var(");
|
|
3966
4385
|
if (child.type === "Declaration") return JSON.stringify(child.value).includes("var(");
|
|
3967
4386
|
return false;
|
|
@@ -3995,9 +4414,9 @@ var applyUsageRule = {
|
|
|
3995
4414
|
"StyleSheet:exit"(node) {
|
|
3996
4415
|
const layers = node.children.filter((child) => child.type === "Atrule" && child.name === "layer");
|
|
3997
4416
|
for (const layer of layers) {
|
|
3998
|
-
for (const child of
|
|
4417
|
+
for (const child of blockChildren2(layer)) {
|
|
3999
4418
|
if (child.type !== "Rule") continue;
|
|
4000
|
-
for (const declaration of
|
|
4419
|
+
for (const declaration of blockChildren2(child)) {
|
|
4001
4420
|
if (declaration.type !== "Declaration") continue;
|
|
4002
4421
|
context.report({
|
|
4003
4422
|
node: declaration,
|
|
@@ -4027,10 +4446,10 @@ var baseLayerPairRule = {
|
|
|
4027
4446
|
if (baseLayers.length === 0) return;
|
|
4028
4447
|
const applied = [];
|
|
4029
4448
|
for (const layer of baseLayers) {
|
|
4030
|
-
for (const child of
|
|
4449
|
+
for (const child of blockChildren2(layer)) {
|
|
4031
4450
|
if (child.type !== "Rule") continue;
|
|
4032
4451
|
if (!selectorNames(child).includes("body")) continue;
|
|
4033
|
-
for (const declaration of
|
|
4452
|
+
for (const declaration of blockChildren2(child)) {
|
|
4034
4453
|
if (declaration.type !== "Atrule" || declaration.name !== "apply") continue;
|
|
4035
4454
|
applied.push(...preludeIdentifiers(declaration));
|
|
4036
4455
|
}
|
|
@@ -4155,7 +4574,7 @@ var customUtilityApplyRule = {
|
|
|
4155
4574
|
return {
|
|
4156
4575
|
"StyleSheet:exit"(node) {
|
|
4157
4576
|
for (const utility of atrulesNamed(node, "utility")) {
|
|
4158
|
-
for (const child of
|
|
4577
|
+
for (const child of blockChildren2(utility)) {
|
|
4159
4578
|
if (child.type === "Atrule" && child.name === "apply") continue;
|
|
4160
4579
|
if (child.type === "Declaration") {
|
|
4161
4580
|
context.report({
|
|
@@ -4227,7 +4646,7 @@ var themeVariableNamespaceRule = {
|
|
|
4227
4646
|
"StyleSheet:exit"(node) {
|
|
4228
4647
|
const namespaces = /* @__PURE__ */ new Set();
|
|
4229
4648
|
for (const theme of atrulesNamed(node, "theme")) {
|
|
4230
|
-
for (const child of
|
|
4649
|
+
for (const child of blockChildren2(theme)) {
|
|
4231
4650
|
if (child.type !== "Declaration") continue;
|
|
4232
4651
|
const segments = child.property.replace(/^--/, "").split("-");
|
|
4233
4652
|
if (segments[0]) namespaces.add(segments[0]);
|
|
@@ -4406,7 +4825,10 @@ var pasikaRules = {
|
|
|
4406
4825
|
"stay-flat": stayFlatRule,
|
|
4407
4826
|
"type-extraction": typeExtractionRule,
|
|
4408
4827
|
"locale-placement": localePlacementRule,
|
|
4828
|
+
"sole-state-owner": soleStateOwnerRule,
|
|
4829
|
+
"locale-key-shape": localeKeyShapeRule,
|
|
4409
4830
|
"shared-style-dedup": sharedStyleDedupRule,
|
|
4831
|
+
"repeated-structure": repeatedStructureRule,
|
|
4410
4832
|
"no-eslint-disable": noEslintDisableRule,
|
|
4411
4833
|
"zod-schema-validation": zodSchemaValidationRule,
|
|
4412
4834
|
"source-under-src": sourceUnderSrcRule
|
|
@@ -4435,7 +4857,7 @@ function hashRequirement(canonicalText) {
|
|
|
4435
4857
|
|
|
4436
4858
|
// enforcement/parse-docs.ts
|
|
4437
4859
|
import { readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
|
|
4438
|
-
import
|
|
4860
|
+
import path38 from "path";
|
|
4439
4861
|
var KIND_BY_SUFFIX2 = [
|
|
4440
4862
|
["-rule.md", "rule"],
|
|
4441
4863
|
["-guide.md", "guide"],
|
|
@@ -4445,7 +4867,7 @@ var KIND_BY_SUFFIX2 = [
|
|
|
4445
4867
|
var RFC_2119 = /\b(?<keyword>MUST NOT|MUST|SHOULD NOT|SHOULD|MAY)\b/;
|
|
4446
4868
|
function listMarkdownFiles2(dir) {
|
|
4447
4869
|
return readdirSync3(dir).flatMap((entry) => {
|
|
4448
|
-
const entryPath =
|
|
4870
|
+
const entryPath = path38.join(dir, entry);
|
|
4449
4871
|
if (statSync4(entryPath).isDirectory()) {
|
|
4450
4872
|
return entry.startsWith("_") ? [] : listMarkdownFiles2(entryPath);
|
|
4451
4873
|
}
|
|
@@ -4471,7 +4893,7 @@ function parseDoc(filePath, docsRoot) {
|
|
|
4471
4893
|
const sourceLines = body.split("\n");
|
|
4472
4894
|
const prose = toProse(body, "$<content>");
|
|
4473
4895
|
const proseWithoutCode = toProse(body, "");
|
|
4474
|
-
const fileName =
|
|
4896
|
+
const fileName = path38.basename(filePath);
|
|
4475
4897
|
const kind = KIND_BY_SUFFIX2.find(([suffix]) => fileName.endsWith(suffix))?.[1];
|
|
4476
4898
|
const requirements = [];
|
|
4477
4899
|
const steps = [];
|
|
@@ -4501,7 +4923,7 @@ function parseDoc(filePath, docsRoot) {
|
|
|
4501
4923
|
const overview = prose.slice(1).find((line) => line.trim() !== "" && !line.startsWith("#"));
|
|
4502
4924
|
return {
|
|
4503
4925
|
filePath,
|
|
4504
|
-
doc:
|
|
4926
|
+
doc: path38.relative(docsRoot, filePath).split(path38.sep).join("/"),
|
|
4505
4927
|
fileName,
|
|
4506
4928
|
kind,
|
|
4507
4929
|
title,
|
|
@@ -4521,7 +4943,7 @@ function parseDocs(docsRoot) {
|
|
|
4521
4943
|
|
|
4522
4944
|
// enforcement/doctor.ts
|
|
4523
4945
|
import fs4 from "fs";
|
|
4524
|
-
import
|
|
4946
|
+
import path39 from "path";
|
|
4525
4947
|
import { z } from "zod";
|
|
4526
4948
|
var doctorCheckRefs = [
|
|
4527
4949
|
"pasika/pasika-installed",
|
|
@@ -4565,7 +4987,7 @@ function checkFrameworkPackages(pkg) {
|
|
|
4565
4987
|
}
|
|
4566
4988
|
function checkSourceRoot(cwd) {
|
|
4567
4989
|
const findings = [];
|
|
4568
|
-
const srcDir =
|
|
4990
|
+
const srcDir = path39.join(cwd, "src");
|
|
4569
4991
|
if (!fs4.existsSync(srcDir)) {
|
|
4570
4992
|
findings.push({
|
|
4571
4993
|
check: "source-under-src",
|
|
@@ -4591,14 +5013,14 @@ function checkSourceRoot(cwd) {
|
|
|
4591
5013
|
function findGlobalStylesheet(cwd) {
|
|
4592
5014
|
const candidates = ["src/app/globals.css", "src/styles/globals.css", "src/globals.css"];
|
|
4593
5015
|
for (const candidate of candidates) {
|
|
4594
|
-
const full =
|
|
5016
|
+
const full = path39.join(cwd, candidate);
|
|
4595
5017
|
if (fs4.existsSync(full)) return full;
|
|
4596
5018
|
}
|
|
4597
|
-
const srcDir =
|
|
5019
|
+
const srcDir = path39.join(cwd, "src");
|
|
4598
5020
|
if (!fs4.existsSync(srcDir)) return void 0;
|
|
4599
5021
|
const walk = (dir) => {
|
|
4600
5022
|
for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
|
|
4601
|
-
const full =
|
|
5023
|
+
const full = path39.join(dir, entry.name);
|
|
4602
5024
|
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".next") {
|
|
4603
5025
|
const found = walk(full);
|
|
4604
5026
|
if (found) return found;
|
|
@@ -4629,7 +5051,7 @@ function checkGlobalStylesheet(cwd) {
|
|
|
4629
5051
|
function checkConfigBaseline(cwd) {
|
|
4630
5052
|
const findings = [];
|
|
4631
5053
|
const eslintConfigs = ["eslint.config.ts", "eslint.config.mjs", "eslint.config.js"];
|
|
4632
|
-
const eslintConfig = eslintConfigs.find((name) => fs4.existsSync(
|
|
5054
|
+
const eslintConfig = eslintConfigs.find((name) => fs4.existsSync(path39.join(cwd, name)));
|
|
4633
5055
|
if (!eslintConfig) {
|
|
4634
5056
|
findings.push({
|
|
4635
5057
|
check: "config-baseline",
|
|
@@ -4637,7 +5059,7 @@ function checkConfigBaseline(cwd) {
|
|
|
4637
5059
|
severity: "error"
|
|
4638
5060
|
});
|
|
4639
5061
|
} else {
|
|
4640
|
-
const content = fs4.readFileSync(
|
|
5062
|
+
const content = fs4.readFileSync(path39.join(cwd, eslintConfig), "utf8");
|
|
4641
5063
|
if (!content.includes("zirka")) {
|
|
4642
5064
|
findings.push({
|
|
4643
5065
|
check: "config-baseline",
|
|
@@ -4646,7 +5068,7 @@ function checkConfigBaseline(cwd) {
|
|
|
4646
5068
|
});
|
|
4647
5069
|
}
|
|
4648
5070
|
}
|
|
4649
|
-
if (!fs4.existsSync(
|
|
5071
|
+
if (!fs4.existsSync(path39.join(cwd, "tsconfig.json"))) {
|
|
4650
5072
|
findings.push({
|
|
4651
5073
|
check: "config-baseline",
|
|
4652
5074
|
message: "No tsconfig.json found. Create one extending the pasika baseline.",
|
|
@@ -4657,9 +5079,9 @@ function checkConfigBaseline(cwd) {
|
|
|
4657
5079
|
}
|
|
4658
5080
|
function checkManagedFiles(cwd) {
|
|
4659
5081
|
const findings = [];
|
|
4660
|
-
const vulykDir =
|
|
5082
|
+
const vulykDir = path39.join(cwd, ".vulyk");
|
|
4661
5083
|
if (!fs4.existsSync(vulykDir)) return findings;
|
|
4662
|
-
const manifestPath =
|
|
5084
|
+
const manifestPath = path39.join(vulykDir, "manifest.json");
|
|
4663
5085
|
if (!fs4.existsSync(manifestPath)) return findings;
|
|
4664
5086
|
try {
|
|
4665
5087
|
const manifestEntrySchema = z.looseObject({
|
|
@@ -4671,7 +5093,7 @@ function checkManagedFiles(cwd) {
|
|
|
4671
5093
|
for (const [, entry] of Object.entries(result.data)) {
|
|
4672
5094
|
const targets = entry.targets ?? [];
|
|
4673
5095
|
for (const target of targets) {
|
|
4674
|
-
const targetPath =
|
|
5096
|
+
const targetPath = path39.join(cwd, target);
|
|
4675
5097
|
if (fs4.existsSync(targetPath)) {
|
|
4676
5098
|
const stat = fs4.statSync(targetPath);
|
|
4677
5099
|
const vulykStat = fs4.statSync(manifestPath);
|
|
@@ -4690,7 +5112,7 @@ function checkManagedFiles(cwd) {
|
|
|
4690
5112
|
return findings;
|
|
4691
5113
|
}
|
|
4692
5114
|
function runDoctor(cwd) {
|
|
4693
|
-
const pkgPath =
|
|
5115
|
+
const pkgPath = path39.join(cwd, "package.json");
|
|
4694
5116
|
const pkg = readPackageJson(pkgPath);
|
|
4695
5117
|
return [
|
|
4696
5118
|
...checkFrameworkPackages(pkg),
|
|
@@ -4738,7 +5160,7 @@ function collectTestTitles(rulesDir) {
|
|
|
4738
5160
|
const titles = /* @__PURE__ */ new Set();
|
|
4739
5161
|
const visit = (dir) => {
|
|
4740
5162
|
for (const entry of readdirSync4(dir)) {
|
|
4741
|
-
const entryPath =
|
|
5163
|
+
const entryPath = path40.join(dir, entry);
|
|
4742
5164
|
if (statSync5(entryPath).isDirectory()) {
|
|
4743
5165
|
visit(entryPath);
|
|
4744
5166
|
continue;
|
|
@@ -4921,12 +5343,12 @@ function json(data) {
|
|
|
4921
5343
|
}
|
|
4922
5344
|
|
|
4923
5345
|
// cli/index.ts
|
|
4924
|
-
var REGISTRY_RELATIVE_PATH =
|
|
5346
|
+
var REGISTRY_RELATIVE_PATH = path41.join("enforcement", "registry.json");
|
|
4925
5347
|
function findRegistryRoot(startDir) {
|
|
4926
|
-
let dir =
|
|
5348
|
+
let dir = path41.resolve(startDir);
|
|
4927
5349
|
for (; ; ) {
|
|
4928
|
-
if (existsSync(
|
|
4929
|
-
const parent =
|
|
5350
|
+
if (existsSync(path41.join(dir, REGISTRY_RELATIVE_PATH))) return dir;
|
|
5351
|
+
const parent = path41.dirname(dir);
|
|
4930
5352
|
if (parent === dir) return void 0;
|
|
4931
5353
|
dir = parent;
|
|
4932
5354
|
}
|
|
@@ -4950,12 +5372,12 @@ program.command("coverage").description("Check that every documented requirement
|
|
|
4950
5372
|
error(`No ${REGISTRY_RELATIVE_PATH} found in this directory or any parent.`);
|
|
4951
5373
|
process.exit(1);
|
|
4952
5374
|
}
|
|
4953
|
-
const docsRoot =
|
|
5375
|
+
const docsRoot = path41.join(root, "docs");
|
|
4954
5376
|
if (!existsSync(docsRoot)) {
|
|
4955
5377
|
error(`No documentation folder at ${docsRoot}. Run coverage inside the pasika repository.`);
|
|
4956
5378
|
process.exit(1);
|
|
4957
5379
|
}
|
|
4958
|
-
const registryPath =
|
|
5380
|
+
const registryPath = path41.join(root, REGISTRY_RELATIVE_PATH);
|
|
4959
5381
|
if (options.classify !== void 0) {
|
|
4960
5382
|
try {
|
|
4961
5383
|
const result = classifyRequirement({
|
|
@@ -4974,7 +5396,7 @@ program.command("coverage").description("Check that every documented requirement
|
|
|
4974
5396
|
const report3 = buildCoverageReport({
|
|
4975
5397
|
docsRoot,
|
|
4976
5398
|
registry: readRegistry(registryPath),
|
|
4977
|
-
rulesDir:
|
|
5399
|
+
rulesDir: path41.join(root, "eslint", "pasika", "rules")
|
|
4978
5400
|
});
|
|
4979
5401
|
if (options.json) {
|
|
4980
5402
|
json(report3);
|