pasika 0.5.18 → 0.6.1
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 +1 -1
- package/dist/eslint/pasika/index.d.ts +6 -0
- package/dist/eslint/pasika/index.js +384 -46
- package/package.json +15 -5
package/README.md
CHANGED
|
@@ -231,7 +231,7 @@ Many of the TS/TSX rules call the TypeScript compiler API directly at lint time,
|
|
|
231
231
|
```bash
|
|
232
232
|
npm run lint
|
|
233
233
|
npm run typecheck
|
|
234
|
-
npm run test
|
|
234
|
+
npm run test:unit
|
|
235
235
|
npm run coverage
|
|
236
236
|
npm run build
|
|
237
237
|
npm run dogfood -- ../some/repo # requires a build; see Dogfooding above
|
|
@@ -98,6 +98,7 @@ declare const documentationRules: {
|
|
|
98
98
|
"no-nested-how-to": _eslint_markdown.MarkdownRuleDefinition;
|
|
99
99
|
"glossary-term-linking": _eslint_markdown.MarkdownRuleDefinition;
|
|
100
100
|
"guide-mentions-documents": _eslint_markdown.MarkdownRuleDefinition;
|
|
101
|
+
"guide-section-shape": _eslint_markdown.MarkdownRuleDefinition;
|
|
101
102
|
};
|
|
102
103
|
|
|
103
104
|
declare const tailwindRules: {
|
|
@@ -119,6 +120,8 @@ declare const tailwindRules: {
|
|
|
119
120
|
declare const repoPackageJsonRules: {
|
|
120
121
|
"no-vulyk-dependency": _eslint_json.JSONRuleDefinition;
|
|
121
122
|
"exact-version": _eslint_json.JSONRuleDefinition;
|
|
123
|
+
"lint-setup": _eslint_json.JSONRuleDefinition;
|
|
124
|
+
"vitest-coverage": _eslint_json.JSONRuleDefinition;
|
|
122
125
|
};
|
|
123
126
|
/** Package.json rules specific to a Next.js (or React) application. */
|
|
124
127
|
declare const nextjsPackageJsonRules: {
|
|
@@ -147,6 +150,8 @@ declare const pasikaPlugin: {
|
|
|
147
150
|
"nextjs-stack": _eslint_json.JSONRuleDefinition;
|
|
148
151
|
"no-vulyk-dependency": _eslint_json.JSONRuleDefinition;
|
|
149
152
|
"exact-version": _eslint_json.JSONRuleDefinition;
|
|
153
|
+
"lint-setup": _eslint_json.JSONRuleDefinition;
|
|
154
|
+
"vitest-coverage": _eslint_json.JSONRuleDefinition;
|
|
150
155
|
"theme-reset": _eslint_css.CSSRuleDefinition;
|
|
151
156
|
"root-variables": _eslint_css.CSSRuleDefinition;
|
|
152
157
|
"apply-usage": _eslint_css.CSSRuleDefinition;
|
|
@@ -182,6 +187,7 @@ declare const pasikaPlugin: {
|
|
|
182
187
|
"no-nested-how-to": _eslint_markdown.MarkdownRuleDefinition;
|
|
183
188
|
"glossary-term-linking": _eslint_markdown.MarkdownRuleDefinition;
|
|
184
189
|
"guide-mentions-documents": _eslint_markdown.MarkdownRuleDefinition;
|
|
190
|
+
"guide-section-shape": _eslint_markdown.MarkdownRuleDefinition;
|
|
185
191
|
"filename-case": eslint.Rule.RuleModule;
|
|
186
192
|
"import-boundaries": eslint.Rule.RuleModule;
|
|
187
193
|
"named-exports": eslint.Rule.RuleModule;
|
|
@@ -174,7 +174,9 @@ function findSimpleRoot(component, _text, _filename) {
|
|
|
174
174
|
};
|
|
175
175
|
visit(body);
|
|
176
176
|
const rendered = returns.filter((statement) => statement.expression?.kind !== ts.SyntaxKind.NullKeyword);
|
|
177
|
-
const roots = rendered.map(
|
|
177
|
+
const roots = rendered.map(
|
|
178
|
+
(statement) => statement.expression && ts.isExpression(statement.expression) ? rootFromExpression(statement.expression) : void 0
|
|
179
|
+
).filter((root) => root !== void 0);
|
|
178
180
|
if (roots.length !== rendered.length || roots.length === 0) return void 0;
|
|
179
181
|
const firstTag = roots[0]?.tagName;
|
|
180
182
|
if (roots.some((root) => root.tagName !== firstTag)) return void 0;
|
|
@@ -2302,7 +2304,17 @@ var NEXT_ROUTING_FILES3 = /* @__PURE__ */ new Set([
|
|
|
2302
2304
|
"sitemap",
|
|
2303
2305
|
"twitter-image"
|
|
2304
2306
|
]);
|
|
2305
|
-
var INTERACTIVE_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
2307
|
+
var INTERACTIVE_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
2308
|
+
"onClick",
|
|
2309
|
+
"onChange",
|
|
2310
|
+
"onSubmit",
|
|
2311
|
+
"href",
|
|
2312
|
+
"onKeyDown",
|
|
2313
|
+
"onKeyUp",
|
|
2314
|
+
"onFocus",
|
|
2315
|
+
"onBlur",
|
|
2316
|
+
"htmlFor"
|
|
2317
|
+
]);
|
|
2306
2318
|
function tagName(node) {
|
|
2307
2319
|
const name = node.openingElement?.name;
|
|
2308
2320
|
if (name?.type !== "JSXIdentifier") return void 0;
|
|
@@ -2375,7 +2387,9 @@ function isDecorative(node) {
|
|
|
2375
2387
|
return !meaningfulText && !isContentElement(name);
|
|
2376
2388
|
}
|
|
2377
2389
|
function isContentElement(name) {
|
|
2378
|
-
return /^(?:h[1-6]|p|li|dt|dd|blockquote|pre|code|figcaption|caption|th|td|form|fieldset|select|textarea)$/.test(
|
|
2390
|
+
return /^(?:h[1-6]|p|li|dt|dd|blockquote|pre|code|figcaption|caption|th|td|form|fieldset|select|textarea)$/.test(
|
|
2391
|
+
name
|
|
2392
|
+
);
|
|
2379
2393
|
}
|
|
2380
2394
|
function isSoleContentOfWrapper(node) {
|
|
2381
2395
|
const parent = node.parent;
|
|
@@ -4153,7 +4167,7 @@ var overviewRule = {
|
|
|
4153
4167
|
meta: {
|
|
4154
4168
|
type: "problem",
|
|
4155
4169
|
docs: {
|
|
4156
|
-
description: "
|
|
4170
|
+
description: "Required documentation overviews must exist, contain at most two sentences, and not link to other documents.",
|
|
4157
4171
|
recommended: true
|
|
4158
4172
|
}
|
|
4159
4173
|
},
|
|
@@ -4196,6 +4210,41 @@ var overviewRule = {
|
|
|
4196
4210
|
if (!found) {
|
|
4197
4211
|
context.report({ node, message: "no overview follows the title" });
|
|
4198
4212
|
}
|
|
4213
|
+
const validateHeadedOverview = (heading, index, label) => {
|
|
4214
|
+
const overview = node.children[index + 1];
|
|
4215
|
+
if (overview?.type !== "paragraph") {
|
|
4216
|
+
context.report({
|
|
4217
|
+
node: heading,
|
|
4218
|
+
message: `no overview follows ${label}`
|
|
4219
|
+
});
|
|
4220
|
+
return;
|
|
4221
|
+
}
|
|
4222
|
+
if (containsLink(overview)) {
|
|
4223
|
+
context.report({
|
|
4224
|
+
node: overview,
|
|
4225
|
+
message: `overview of ${label} links another document`
|
|
4226
|
+
});
|
|
4227
|
+
}
|
|
4228
|
+
const sectionSentenceCount = countSentences(getTextContent(overview).trim());
|
|
4229
|
+
if (sectionSentenceCount > 2) {
|
|
4230
|
+
context.report({
|
|
4231
|
+
node: overview,
|
|
4232
|
+
message: `overview of ${label} uses ${String(sectionSentenceCount)} sentences, at most two are allowed`
|
|
4233
|
+
});
|
|
4234
|
+
}
|
|
4235
|
+
};
|
|
4236
|
+
const isGuide = filename.endsWith("-guide.md");
|
|
4237
|
+
const isReference = filename.endsWith("-reference.md");
|
|
4238
|
+
if (!isGuide && !isReference) return;
|
|
4239
|
+
for (const [index, child] of node.children.entries()) {
|
|
4240
|
+
if (child.type !== "heading" || child.depth === 1) continue;
|
|
4241
|
+
const title = getTextContent(child).trim();
|
|
4242
|
+
if (isGuide && child.depth === 2 && /^How To \S/.test(title)) {
|
|
4243
|
+
validateHeadedOverview(child, index, `guide section "${title}"`);
|
|
4244
|
+
} else if (isReference) {
|
|
4245
|
+
validateHeadedOverview(child, index, `reference block "${title}"`);
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4199
4248
|
}
|
|
4200
4249
|
};
|
|
4201
4250
|
}
|
|
@@ -5049,6 +5098,56 @@ var guideMentionsDocumentsRule = {
|
|
|
5049
5098
|
}
|
|
5050
5099
|
};
|
|
5051
5100
|
|
|
5101
|
+
// eslint/rules/documentation/guide-section-shape.ts
|
|
5102
|
+
var guideSectionShapeRule = {
|
|
5103
|
+
meta: {
|
|
5104
|
+
type: "problem",
|
|
5105
|
+
docs: {
|
|
5106
|
+
description: "Guide sections use How To headings, an overview, and numbered steps.",
|
|
5107
|
+
recommended: true
|
|
5108
|
+
}
|
|
5109
|
+
},
|
|
5110
|
+
create(context) {
|
|
5111
|
+
return {
|
|
5112
|
+
root(node) {
|
|
5113
|
+
if (!getFilename(context).endsWith("-guide.md")) return;
|
|
5114
|
+
const sections = [];
|
|
5115
|
+
for (const [index, child] of node.children.entries()) {
|
|
5116
|
+
if (child.type !== "heading" || child.depth === 1) continue;
|
|
5117
|
+
sections.push({ heading: child, index, title: getTextContent(child).trim() });
|
|
5118
|
+
}
|
|
5119
|
+
if (sections.length === 0) {
|
|
5120
|
+
context.report({ node, message: "guide must contain at least one How To section" });
|
|
5121
|
+
return;
|
|
5122
|
+
}
|
|
5123
|
+
for (const [sectionIndex, section] of sections.entries()) {
|
|
5124
|
+
if (section.heading.depth !== 2) {
|
|
5125
|
+
context.report({
|
|
5126
|
+
node: section.heading,
|
|
5127
|
+
message: `guide section heading "${section.title}" must be level two`
|
|
5128
|
+
});
|
|
5129
|
+
}
|
|
5130
|
+
if (!/^How To \S/.test(section.title)) {
|
|
5131
|
+
context.report({
|
|
5132
|
+
node: section.heading,
|
|
5133
|
+
message: `guide section heading "${section.title}" must start with "How To "`
|
|
5134
|
+
});
|
|
5135
|
+
}
|
|
5136
|
+
const nextSection = sections[sectionIndex + 1];
|
|
5137
|
+
const content = node.children.slice(section.index + 1, nextSection?.index ?? node.children.length);
|
|
5138
|
+
const hasOverviewThenNumberedList = content.length === 2 && content[0]?.type === "paragraph" && content[1]?.type === "list" && content[1].ordered === true;
|
|
5139
|
+
if (!hasOverviewThenNumberedList) {
|
|
5140
|
+
context.report({
|
|
5141
|
+
node: section.heading,
|
|
5142
|
+
message: `guide section "${section.title}" must consist of its overview followed by one numbered list`
|
|
5143
|
+
});
|
|
5144
|
+
}
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5147
|
+
};
|
|
5148
|
+
}
|
|
5149
|
+
};
|
|
5150
|
+
|
|
5052
5151
|
// eslint/rules/documentation/index.ts
|
|
5053
5152
|
var documentationRules = {
|
|
5054
5153
|
"doc-kind-suffix": docKindSuffixRule,
|
|
@@ -5073,7 +5172,8 @@ var documentationRules = {
|
|
|
5073
5172
|
"guide-link-anchors": guideLinkAnchorsRule,
|
|
5074
5173
|
"no-nested-how-to": noNestedHowToRule,
|
|
5075
5174
|
"glossary-term-linking": glossaryTermLinkingRule,
|
|
5076
|
-
"guide-mentions-documents": guideMentionsDocumentsRule
|
|
5175
|
+
"guide-mentions-documents": guideMentionsDocumentsRule,
|
|
5176
|
+
"guide-section-shape": guideSectionShapeRule
|
|
5077
5177
|
};
|
|
5078
5178
|
|
|
5079
5179
|
// eslint/rules/tailwind/helpers.ts
|
|
@@ -5786,10 +5886,108 @@ var exactVersionRule = {
|
|
|
5786
5886
|
}
|
|
5787
5887
|
};
|
|
5788
5888
|
|
|
5789
|
-
// eslint/rules/package-json/
|
|
5889
|
+
// eslint/rules/package-json/lint-setup.ts
|
|
5790
5890
|
function memberName2(member) {
|
|
5791
5891
|
return member.name.type === "String" ? member.name.value : member.name.name;
|
|
5792
5892
|
}
|
|
5893
|
+
function memberValue2(member) {
|
|
5894
|
+
return member?.value.type === "String" ? member.value.value : void 0;
|
|
5895
|
+
}
|
|
5896
|
+
function stringValues(value) {
|
|
5897
|
+
if (value.type === "String") return [value.value];
|
|
5898
|
+
if (value.type !== "Array") return [];
|
|
5899
|
+
return value.elements.flatMap((element) => element.value.type === "String" ? [element.value.value] : []);
|
|
5900
|
+
}
|
|
5901
|
+
var SOURCE_GLOB_PATTERN = /(?:^|[^a-z])(?:[cm]?[jt]sx?)(?:[^a-z]|$)/i;
|
|
5902
|
+
var DIRECT_ESLINT_PATTERN = /(?:^|&&|\|\||;)\s*(?:npx\s+)?eslint(?:\s|$)/;
|
|
5903
|
+
var REPOSITORY_ARGUMENT_PATTERN = /(?:^|\s)\.(?=\s|$)/;
|
|
5904
|
+
var PRETTIER_PATTERN = /\bprettier\b/;
|
|
5905
|
+
var CHECK_FLAG_PATTERN = /(?:^|\s)(?:--check|-c)(?:\s|$)/;
|
|
5906
|
+
function runsEslintDirectly(command) {
|
|
5907
|
+
return DIRECT_ESLINT_PATTERN.test(command);
|
|
5908
|
+
}
|
|
5909
|
+
function runsPrettier(command) {
|
|
5910
|
+
return PRETTIER_PATTERN.test(command);
|
|
5911
|
+
}
|
|
5912
|
+
function runsNamedScript(command, name) {
|
|
5913
|
+
return command.includes(`npm run ${name}`);
|
|
5914
|
+
}
|
|
5915
|
+
var lintSetupRule = {
|
|
5916
|
+
meta: {
|
|
5917
|
+
schema: [],
|
|
5918
|
+
type: "problem",
|
|
5919
|
+
docs: {
|
|
5920
|
+
description: "Require full-repository lint/format scripts and argument-free staged-file scripts that lint-staged runs by name."
|
|
5921
|
+
}
|
|
5922
|
+
},
|
|
5923
|
+
create(context) {
|
|
5924
|
+
return {
|
|
5925
|
+
Document(node) {
|
|
5926
|
+
const root = node.body;
|
|
5927
|
+
if (root.type !== "Object") return;
|
|
5928
|
+
const scripts = root.members.find((member) => memberName2(member) === "scripts");
|
|
5929
|
+
const scriptMembers = scripts?.value.type === "Object" ? scripts.value.members : [];
|
|
5930
|
+
const findScript = (name) => scriptMembers.find((member) => memberName2(member) === name);
|
|
5931
|
+
const lint = findScript("lint");
|
|
5932
|
+
const lintCommand = memberValue2(lint);
|
|
5933
|
+
if (lintCommand === void 0 || !runsEslintDirectly(lintCommand) || !REPOSITORY_ARGUMENT_PATTERN.test(lintCommand)) {
|
|
5934
|
+
context.report({
|
|
5935
|
+
node: lint ?? node,
|
|
5936
|
+
message: 'package.json must declare a "lint" script that runs ESLint across the repository (e.g. "eslint .").'
|
|
5937
|
+
});
|
|
5938
|
+
}
|
|
5939
|
+
const format = findScript("format");
|
|
5940
|
+
const formatCommand = memberValue2(format);
|
|
5941
|
+
if (formatCommand === void 0 || !runsPrettier(formatCommand) || !CHECK_FLAG_PATTERN.test(formatCommand) || !REPOSITORY_ARGUMENT_PATTERN.test(formatCommand)) {
|
|
5942
|
+
context.report({
|
|
5943
|
+
node: format ?? node,
|
|
5944
|
+
message: 'package.json must declare a "format" script that runs prettier --check across the repository.'
|
|
5945
|
+
});
|
|
5946
|
+
}
|
|
5947
|
+
const lintStaged = findScript("lint:staged");
|
|
5948
|
+
const lintStagedCommand = memberValue2(lintStaged);
|
|
5949
|
+
if (lintStagedCommand === void 0 || !runsEslintDirectly(lintStagedCommand) || REPOSITORY_ARGUMENT_PATTERN.test(lintStagedCommand)) {
|
|
5950
|
+
context.report({
|
|
5951
|
+
node: lintStaged ?? node,
|
|
5952
|
+
message: 'package.json must declare a "lint:staged" script that runs ESLint with no repository-wide argument (e.g. "eslint --fix").'
|
|
5953
|
+
});
|
|
5954
|
+
}
|
|
5955
|
+
const formatStaged = findScript("format:staged");
|
|
5956
|
+
const formatStagedCommand = memberValue2(formatStaged);
|
|
5957
|
+
if (formatStagedCommand === void 0 || !runsPrettier(formatStagedCommand) || REPOSITORY_ARGUMENT_PATTERN.test(formatStagedCommand)) {
|
|
5958
|
+
context.report({
|
|
5959
|
+
node: formatStaged ?? node,
|
|
5960
|
+
message: 'package.json must declare a "format:staged" script that runs prettier with no repository-wide argument (e.g. "prettier --write").'
|
|
5961
|
+
});
|
|
5962
|
+
}
|
|
5963
|
+
const lintStagedConfig = root.members.find((member) => memberName2(member) === "lint-staged");
|
|
5964
|
+
const runsStagedScript = (name) => lintStagedConfig?.value.type === "Object" && lintStagedConfig.value.members.some(
|
|
5965
|
+
(member) => stringValues(member.value).some((command) => runsNamedScript(command, name))
|
|
5966
|
+
);
|
|
5967
|
+
const hasStagedLintEntry = lintStagedConfig?.value.type === "Object" && lintStagedConfig.value.members.some(
|
|
5968
|
+
(member) => SOURCE_GLOB_PATTERN.test(memberName2(member)) && stringValues(member.value).some((command) => runsNamedScript(command, "lint:staged"))
|
|
5969
|
+
);
|
|
5970
|
+
if (!hasStagedLintEntry) {
|
|
5971
|
+
context.report({
|
|
5972
|
+
node: lintStagedConfig ?? node,
|
|
5973
|
+
message: 'package.json lint-staged must run "npm run lint:staged --" for staged JavaScript or TypeScript files.'
|
|
5974
|
+
});
|
|
5975
|
+
}
|
|
5976
|
+
if (!runsStagedScript("format:staged")) {
|
|
5977
|
+
context.report({
|
|
5978
|
+
node: lintStagedConfig ?? node,
|
|
5979
|
+
message: 'package.json lint-staged must run "npm run format:staged --" for staged files ESLint does not already format.'
|
|
5980
|
+
});
|
|
5981
|
+
}
|
|
5982
|
+
}
|
|
5983
|
+
};
|
|
5984
|
+
}
|
|
5985
|
+
};
|
|
5986
|
+
|
|
5987
|
+
// eslint/rules/package-json/no-vulyk-dependency.ts
|
|
5988
|
+
function memberName3(member) {
|
|
5989
|
+
return member.name.type === "String" ? member.name.value : member.name.name;
|
|
5990
|
+
}
|
|
5793
5991
|
var noVulykDependencyRule = {
|
|
5794
5992
|
meta: {
|
|
5795
5993
|
schema: [],
|
|
@@ -5804,9 +6002,9 @@ var noVulykDependencyRule = {
|
|
|
5804
6002
|
const root = node.body;
|
|
5805
6003
|
if (root.type !== "Object") return;
|
|
5806
6004
|
for (const key of ["dependencies", "devDependencies"]) {
|
|
5807
|
-
const section = root.members.find((member) =>
|
|
6005
|
+
const section = root.members.find((member) => memberName3(member) === key);
|
|
5808
6006
|
if (section?.value.type !== "Object") continue;
|
|
5809
|
-
const vulyk = section.value.members.find((member) =>
|
|
6007
|
+
const vulyk = section.value.members.find((member) => memberName3(member) === "vulyk");
|
|
5810
6008
|
if (vulyk) {
|
|
5811
6009
|
context.report({
|
|
5812
6010
|
node: vulyk,
|
|
@@ -5836,15 +6034,19 @@ var NEXTJS_STACK_DEV_DEPENDENCIES = [
|
|
|
5836
6034
|
"prettier",
|
|
5837
6035
|
"husky",
|
|
5838
6036
|
"lint-staged",
|
|
5839
|
-
"zirka"
|
|
6037
|
+
"zirka",
|
|
6038
|
+
"jsdom",
|
|
6039
|
+
"@vitejs/plugin-react",
|
|
6040
|
+
"@testing-library/react",
|
|
6041
|
+
"@testing-library/dom"
|
|
5840
6042
|
];
|
|
5841
|
-
function
|
|
6043
|
+
function memberName4(member) {
|
|
5842
6044
|
return member.name.type === "String" ? member.name.value : member.name.name;
|
|
5843
6045
|
}
|
|
5844
6046
|
function sectionPackages(root, section) {
|
|
5845
|
-
const member = root.type === "Object" ? root.members.find((entry) =>
|
|
6047
|
+
const member = root.type === "Object" ? root.members.find((entry) => memberName4(entry) === section) : void 0;
|
|
5846
6048
|
if (member?.value.type !== "Object") return /* @__PURE__ */ new Set();
|
|
5847
|
-
return new Set(member.value.members.map(
|
|
6049
|
+
return new Set(member.value.members.map(memberName4));
|
|
5848
6050
|
}
|
|
5849
6051
|
var nextjsStackRule = {
|
|
5850
6052
|
meta: {
|
|
@@ -5880,19 +6082,151 @@ var nextjsStackRule = {
|
|
|
5880
6082
|
}
|
|
5881
6083
|
};
|
|
5882
6084
|
|
|
6085
|
+
// eslint/rules/package-json/vitest-coverage.ts
|
|
6086
|
+
import { existsSync as existsSync2, readFileSync as readFileSync9 } from "fs";
|
|
6087
|
+
import path47 from "path";
|
|
6088
|
+
function memberName5(member) {
|
|
6089
|
+
return member.name.type === "String" ? member.name.value : member.name.name;
|
|
6090
|
+
}
|
|
6091
|
+
function memberValue3(member) {
|
|
6092
|
+
return member?.value.type === "String" ? member.value.value : void 0;
|
|
6093
|
+
}
|
|
6094
|
+
function stringValues2(value) {
|
|
6095
|
+
if (value.type === "String") return [value.value];
|
|
6096
|
+
if (value.type !== "Array") return [];
|
|
6097
|
+
return value.elements.flatMap((element) => element.value.type === "String" ? [element.value.value] : []);
|
|
6098
|
+
}
|
|
6099
|
+
var VITEST_CONFIG_NAMES = [
|
|
6100
|
+
"vitest.config.ts",
|
|
6101
|
+
"vitest.config.mts",
|
|
6102
|
+
"vitest.config.cts",
|
|
6103
|
+
"vitest.config.js",
|
|
6104
|
+
"vitest.config.mjs",
|
|
6105
|
+
"vitest.config.cjs"
|
|
6106
|
+
];
|
|
6107
|
+
var THRESHOLD_METRICS = ["lines", "functions", "branches", "statements"];
|
|
6108
|
+
var SOURCE_GLOB_PATTERN2 = /(?:^|[^a-z])(?:[cm]?[jt]sx?)(?:[^a-z]|$)/i;
|
|
6109
|
+
var COVERAGE_FLAG_PATTERN = /(?:^|\s)--coverage(?:[=\s]|$)/;
|
|
6110
|
+
var RELATED_PATTERN = /\brelated\b/;
|
|
6111
|
+
var AUTO_UPDATE_PATTERN = /autoUpdate\s*:\s*true/;
|
|
6112
|
+
var CHANGED_FLAG_PATTERN = /--coverage\.changed\b/;
|
|
6113
|
+
var PER_FILE_FLAG_PATTERN = /--coverage\.thresholds\.perFile\b/;
|
|
6114
|
+
var AUTO_UPDATE_DISABLED_FLAG_PATTERN = /--coverage\.thresholds\.autoUpdate=false\b/;
|
|
6115
|
+
function hasEightyOrAboveFlag(command, metric) {
|
|
6116
|
+
return new RegExp(`--coverage\\.thresholds\\.${metric}=(?:8\\d|9\\d|100)\\b`).test(command);
|
|
6117
|
+
}
|
|
6118
|
+
var vitestCoverageRule = {
|
|
6119
|
+
meta: {
|
|
6120
|
+
schema: [],
|
|
6121
|
+
type: "problem",
|
|
6122
|
+
docs: {
|
|
6123
|
+
description: "Require Vitest unit-test scripts, the V8 provider, a rising coverage threshold, and an 80% floor on staged files via lint-staged."
|
|
6124
|
+
}
|
|
6125
|
+
},
|
|
6126
|
+
create(context) {
|
|
6127
|
+
return {
|
|
6128
|
+
Document(node) {
|
|
6129
|
+
const root = node.body;
|
|
6130
|
+
if (root.type !== "Object") return;
|
|
6131
|
+
const devDependencies = root.members.find((member) => memberName5(member) === "devDependencies");
|
|
6132
|
+
const devDependencyNames = new Set(
|
|
6133
|
+
devDependencies?.value.type === "Object" ? devDependencies.value.members.map(memberName5) : []
|
|
6134
|
+
);
|
|
6135
|
+
if (!devDependencyNames.has("vitest")) {
|
|
6136
|
+
context.report({ node, message: "vitest must be listed in package.json as a devDependency." });
|
|
6137
|
+
}
|
|
6138
|
+
if (!devDependencyNames.has("@vitest/coverage-v8")) {
|
|
6139
|
+
context.report({
|
|
6140
|
+
node,
|
|
6141
|
+
message: "@vitest/coverage-v8 must be listed in package.json as a devDependency."
|
|
6142
|
+
});
|
|
6143
|
+
}
|
|
6144
|
+
const scripts = root.members.find((member) => memberName5(member) === "scripts");
|
|
6145
|
+
const scriptMembers = scripts?.value.type === "Object" ? scripts.value.members : [];
|
|
6146
|
+
const unit = scriptMembers.find((member) => memberName5(member) === "test:unit");
|
|
6147
|
+
const unitCommand = memberValue3(unit);
|
|
6148
|
+
if (unitCommand === void 0 || !/\bvitest\b/.test(unitCommand) || COVERAGE_FLAG_PATTERN.test(unitCommand)) {
|
|
6149
|
+
context.report({
|
|
6150
|
+
node: unit ?? node,
|
|
6151
|
+
message: 'package.json must declare a "test:unit" script that runs Vitest without coverage.'
|
|
6152
|
+
});
|
|
6153
|
+
}
|
|
6154
|
+
const coverage = scriptMembers.find((member) => memberName5(member) === "test:unit:coverage");
|
|
6155
|
+
const coverageCommand = memberValue3(coverage);
|
|
6156
|
+
if (coverageCommand === void 0 || !/\bvitest\b/.test(coverageCommand) || !COVERAGE_FLAG_PATTERN.test(coverageCommand)) {
|
|
6157
|
+
context.report({
|
|
6158
|
+
node: coverage ?? node,
|
|
6159
|
+
message: 'package.json must declare a "test:unit:coverage" script that runs Vitest with coverage.'
|
|
6160
|
+
});
|
|
6161
|
+
}
|
|
6162
|
+
const configName = VITEST_CONFIG_NAMES.find((name) => existsSync2(path47.join(context.cwd, name)));
|
|
6163
|
+
if (!configName) {
|
|
6164
|
+
context.report({
|
|
6165
|
+
node,
|
|
6166
|
+
message: "No vitest config found. Create one with a coverage threshold above zero for lines, functions, branches, and statements."
|
|
6167
|
+
});
|
|
6168
|
+
return;
|
|
6169
|
+
}
|
|
6170
|
+
const content = readFileSync9(path47.join(context.cwd, configName), "utf8");
|
|
6171
|
+
for (const metric of THRESHOLD_METRICS) {
|
|
6172
|
+
if (!new RegExp(`\\b${metric}\\s*:\\s*[1-9]\\d*`).test(content)) {
|
|
6173
|
+
context.report({ node, message: `${configName} must set a coverage threshold above zero for ${metric}.` });
|
|
6174
|
+
}
|
|
6175
|
+
}
|
|
6176
|
+
if (!AUTO_UPDATE_PATTERN.test(content)) {
|
|
6177
|
+
context.report({
|
|
6178
|
+
node,
|
|
6179
|
+
message: `${configName} must set coverage.thresholds.autoUpdate to true.`
|
|
6180
|
+
});
|
|
6181
|
+
}
|
|
6182
|
+
const changedCoverage = scriptMembers.find((member) => memberName5(member) === "test:unit:coverage:staged");
|
|
6183
|
+
const changedCoverageCommand = memberValue3(changedCoverage);
|
|
6184
|
+
if (changedCoverageCommand === void 0 || !/\bvitest\b/.test(changedCoverageCommand) || !RELATED_PATTERN.test(changedCoverageCommand) || !COVERAGE_FLAG_PATTERN.test(changedCoverageCommand)) {
|
|
6185
|
+
context.report({
|
|
6186
|
+
node: changedCoverage ?? node,
|
|
6187
|
+
message: 'package.json must declare a "test:unit:coverage:staged" script that runs vitest related with coverage.'
|
|
6188
|
+
});
|
|
6189
|
+
}
|
|
6190
|
+
const lintStaged = root.members.find((member) => memberName5(member) === "lint-staged");
|
|
6191
|
+
const hasStagedChangedCoverage = lintStaged?.value.type === "Object" && lintStaged.value.members.some(
|
|
6192
|
+
(member) => SOURCE_GLOB_PATTERN2.test(memberName5(member)) && stringValues2(member.value).some((command) => command.includes("npm run test:unit:coverage:staged"))
|
|
6193
|
+
);
|
|
6194
|
+
if (!hasStagedChangedCoverage) {
|
|
6195
|
+
context.report({
|
|
6196
|
+
node: lintStaged ?? node,
|
|
6197
|
+
message: 'package.json lint-staged must run "npm run test:unit:coverage:staged" for staged JavaScript or TypeScript files.'
|
|
6198
|
+
});
|
|
6199
|
+
}
|
|
6200
|
+
const changedCoverageCommandText = changedCoverageCommand ?? "";
|
|
6201
|
+
const hasEightyFloor = THRESHOLD_METRICS.every(
|
|
6202
|
+
(metric) => hasEightyOrAboveFlag(changedCoverageCommandText, metric)
|
|
6203
|
+
);
|
|
6204
|
+
if (!CHANGED_FLAG_PATTERN.test(changedCoverageCommandText) || !PER_FILE_FLAG_PATTERN.test(changedCoverageCommandText) || !hasEightyFloor || !AUTO_UPDATE_DISABLED_FLAG_PATTERN.test(changedCoverageCommandText)) {
|
|
6205
|
+
context.report({
|
|
6206
|
+
node: changedCoverage ?? node,
|
|
6207
|
+
message: 'package.json "test:unit:coverage:staged" script must pass --coverage.changed, a --coverage.thresholds.perFile of at least 80 for lines, functions, branches, and statements, and --coverage.thresholds.autoUpdate=false.'
|
|
6208
|
+
});
|
|
6209
|
+
}
|
|
6210
|
+
}
|
|
6211
|
+
};
|
|
6212
|
+
}
|
|
6213
|
+
};
|
|
6214
|
+
|
|
5883
6215
|
// eslint/rules/package-json/index.ts
|
|
5884
6216
|
var repoPackageJsonRules = {
|
|
5885
6217
|
"no-vulyk-dependency": noVulykDependencyRule,
|
|
5886
|
-
"exact-version": exactVersionRule
|
|
6218
|
+
"exact-version": exactVersionRule,
|
|
6219
|
+
"lint-setup": lintSetupRule,
|
|
6220
|
+
"vitest-coverage": vitestCoverageRule
|
|
5887
6221
|
};
|
|
5888
6222
|
var nextjsPackageJsonRules = {
|
|
5889
6223
|
"nextjs-stack": nextjsStackRule
|
|
5890
6224
|
};
|
|
5891
6225
|
|
|
5892
6226
|
// eslint/rules/husky/husky-hook.ts
|
|
5893
|
-
import { existsSync as
|
|
5894
|
-
import
|
|
5895
|
-
function
|
|
6227
|
+
import { existsSync as existsSync3, readFileSync as readFileSync10 } from "fs";
|
|
6228
|
+
import path48 from "path";
|
|
6229
|
+
function memberName6(member) {
|
|
5896
6230
|
return member.name.type === "String" ? member.name.value : member.name.name;
|
|
5897
6231
|
}
|
|
5898
6232
|
var huskyHookRule = {
|
|
@@ -5900,7 +6234,7 @@ var huskyHookRule = {
|
|
|
5900
6234
|
schema: [],
|
|
5901
6235
|
type: "problem",
|
|
5902
6236
|
docs: {
|
|
5903
|
-
description: "Require a pre-commit hook that runs lint-staged
|
|
6237
|
+
description: "Require a pre-commit hook that runs lint-staged and the libyear drift check directly, and the typecheck and suppression-file ratchet through named package.json scripts."
|
|
5904
6238
|
}
|
|
5905
6239
|
},
|
|
5906
6240
|
create(context) {
|
|
@@ -5908,29 +6242,39 @@ var huskyHookRule = {
|
|
|
5908
6242
|
Document(node) {
|
|
5909
6243
|
const root = node.body;
|
|
5910
6244
|
if (root.type !== "Object") return;
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
if (!existsSync2(hookPath)) {
|
|
6245
|
+
if (!context.filename.endsWith("package.json")) return;
|
|
6246
|
+
const hookPath = path48.join(context.cwd, ".husky", "pre-commit");
|
|
6247
|
+
if (!existsSync3(hookPath)) {
|
|
5915
6248
|
context.report({
|
|
5916
6249
|
node,
|
|
5917
6250
|
message: "No .husky/pre-commit hook found. Configure husky to run checks before commits."
|
|
5918
6251
|
});
|
|
5919
6252
|
return;
|
|
5920
6253
|
}
|
|
5921
|
-
const content =
|
|
6254
|
+
const content = readFileSync10(hookPath, "utf8");
|
|
6255
|
+
const scripts = root.members.find((member) => memberName6(member) === "scripts");
|
|
6256
|
+
const scriptNames = new Set(scripts?.value.type === "Object" ? scripts.value.members.map(memberName6) : []);
|
|
6257
|
+
const requireNamedScript = (name) => {
|
|
6258
|
+
if (!scriptNames.has(name)) {
|
|
6259
|
+
context.report({ node, message: `package.json must declare a "${name}" script.` });
|
|
6260
|
+
}
|
|
6261
|
+
if (!content.includes(`npm run ${name}`)) {
|
|
6262
|
+
context.report({ node, message: `.husky/pre-commit must run npm run ${name}.` });
|
|
6263
|
+
}
|
|
6264
|
+
};
|
|
5922
6265
|
if (!content.includes("lint-staged")) {
|
|
5923
6266
|
context.report({ node, message: ".husky/pre-commit must run lint-staged." });
|
|
5924
6267
|
}
|
|
5925
|
-
|
|
5926
|
-
context.report({ node, message: ".husky/pre-commit must run npm run typecheck." });
|
|
5927
|
-
}
|
|
6268
|
+
requireNamedScript("typecheck");
|
|
5928
6269
|
if (!content.includes("libyear --limit-major-individual=1")) {
|
|
5929
6270
|
context.report({ node, message: ".husky/pre-commit must run npx libyear --limit-major-individual=1." });
|
|
5930
6271
|
}
|
|
5931
|
-
const
|
|
6272
|
+
const suppressionsPath = path48.join(context.cwd, "eslint-suppressions.json");
|
|
6273
|
+
if (existsSync3(suppressionsPath)) {
|
|
6274
|
+
requireNamedScript("lint:prune");
|
|
6275
|
+
}
|
|
5932
6276
|
if (scripts?.value.type === "Object") {
|
|
5933
|
-
const prepare = scripts.value.members.find((member) =>
|
|
6277
|
+
const prepare = scripts.value.members.find((member) => memberName6(member) === "prepare");
|
|
5934
6278
|
if (prepare?.value.type === "String" && !prepare.value.value.includes("husky")) {
|
|
5935
6279
|
context.report({
|
|
5936
6280
|
node: prepare,
|
|
@@ -5949,8 +6293,8 @@ var huskyRules = {
|
|
|
5949
6293
|
};
|
|
5950
6294
|
|
|
5951
6295
|
// eslint/rules/vulyk/vulyk-docs.ts
|
|
5952
|
-
import { existsSync as
|
|
5953
|
-
import
|
|
6296
|
+
import { existsSync as existsSync4, readFileSync as readFileSync11 } from "fs";
|
|
6297
|
+
import path49 from "path";
|
|
5954
6298
|
var PASIKA_REPO = "Bredansky/pasika";
|
|
5955
6299
|
var BASE_REQUIRED_DOCS = [
|
|
5956
6300
|
{ name: "documentation-guide", path: "docs/documentation-guide" },
|
|
@@ -5961,13 +6305,13 @@ var NEXTJS_REQUIRED_DOCS = [
|
|
|
5961
6305
|
{ name: "next-codebase-guide", path: "docs/next-codebase-guide" },
|
|
5962
6306
|
{ name: "next-tailwind-guide", path: "docs/next-tailwind-guide" }
|
|
5963
6307
|
];
|
|
5964
|
-
function
|
|
6308
|
+
function memberName7(member) {
|
|
5965
6309
|
return member.name.type === "String" ? member.name.value : member.name.name;
|
|
5966
6310
|
}
|
|
5967
6311
|
function hasDependency(root, name) {
|
|
5968
|
-
const section = root.members.find((member) =>
|
|
6312
|
+
const section = root.members.find((member) => memberName7(member) === "dependencies");
|
|
5969
6313
|
if (section?.value.type !== "Object") return false;
|
|
5970
|
-
return section.value.members.some((member) =>
|
|
6314
|
+
return section.value.members.some((member) => memberName7(member) === name);
|
|
5971
6315
|
}
|
|
5972
6316
|
var vulykDocsRule = {
|
|
5973
6317
|
meta: {
|
|
@@ -5983,16 +6327,16 @@ var vulykDocsRule = {
|
|
|
5983
6327
|
if (!context.filename.endsWith("package.json")) return;
|
|
5984
6328
|
const root = node.body;
|
|
5985
6329
|
if (root.type !== "Object") return;
|
|
5986
|
-
const projectRoot =
|
|
5987
|
-
const configPath =
|
|
5988
|
-
if (!
|
|
6330
|
+
const projectRoot = path49.dirname(path49.resolve(context.filename));
|
|
6331
|
+
const configPath = path49.join(projectRoot, "vulyk.config.ts");
|
|
6332
|
+
if (!existsSync4(configPath)) {
|
|
5989
6333
|
context.report({
|
|
5990
6334
|
node,
|
|
5991
6335
|
message: "No vulyk.config.ts found. Run npx vulyk@latest init to create one that tracks the framework's docs."
|
|
5992
6336
|
});
|
|
5993
6337
|
return;
|
|
5994
6338
|
}
|
|
5995
|
-
const config =
|
|
6339
|
+
const config = readFileSync11(configPath, "utf8");
|
|
5996
6340
|
if (!config.includes(PASIKA_REPO)) {
|
|
5997
6341
|
context.report({
|
|
5998
6342
|
node,
|
|
@@ -6009,8 +6353,8 @@ var vulykDocsRule = {
|
|
|
6009
6353
|
});
|
|
6010
6354
|
}
|
|
6011
6355
|
}
|
|
6012
|
-
const agentsPath =
|
|
6013
|
-
if (!
|
|
6356
|
+
const agentsPath = path49.join(projectRoot, "AGENTS.md");
|
|
6357
|
+
if (!existsSync4(agentsPath)) {
|
|
6014
6358
|
context.report({
|
|
6015
6359
|
node,
|
|
6016
6360
|
message: "No AGENTS.md found. Run npx vulyk@latest agents to generate the agent file that routes to the tracked docs."
|
|
@@ -6130,9 +6474,7 @@ var tailwindStructureRules = {
|
|
|
6130
6474
|
},
|
|
6131
6475
|
language: "css/css",
|
|
6132
6476
|
languageOptions: { tolerant: true },
|
|
6133
|
-
rules: Object.fromEntries(
|
|
6134
|
-
tailwindRuleIds.filter((id) => id !== "pasika/css-entry-point").map((id) => [id, "error"])
|
|
6135
|
-
)
|
|
6477
|
+
rules: Object.fromEntries(tailwindRuleIds.filter((id) => id !== "pasika/css-entry-point").map((id) => [id, "error"]))
|
|
6136
6478
|
};
|
|
6137
6479
|
var tailwindImportGraph = {
|
|
6138
6480
|
files: ["src/**/*.css"],
|
|
@@ -6180,11 +6522,7 @@ var documentationConfig = {
|
|
|
6180
6522
|
language: "markdown/gfm",
|
|
6181
6523
|
rules: Object.fromEntries(documentationRuleIds.map((id) => [id, "error"]))
|
|
6182
6524
|
};
|
|
6183
|
-
var pasikaApp = [
|
|
6184
|
-
pasikaAppPackageJsonConfig,
|
|
6185
|
-
zirkaConfig,
|
|
6186
|
-
documentationConfig
|
|
6187
|
-
];
|
|
6525
|
+
var pasikaApp = [pasikaAppPackageJsonConfig, zirkaConfig, documentationConfig];
|
|
6188
6526
|
var pasikaNextjsAppWithDiagnostic = (preset) => {
|
|
6189
6527
|
let checked = false;
|
|
6190
6528
|
return new Proxy(preset, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pasika",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Reusable agent setup package",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,15 +22,23 @@
|
|
|
22
22
|
"coverage": "tsx scripts/coverage.ts",
|
|
23
23
|
"dogfood": "tsx scripts/dogfood.ts",
|
|
24
24
|
"fix": "eslint . --fix",
|
|
25
|
+
"format": "prettier --check . --ignore-unknown",
|
|
26
|
+
"format:staged": "prettier --write --ignore-unknown",
|
|
25
27
|
"lint": "eslint .",
|
|
28
|
+
"lint:staged": "eslint --fix",
|
|
26
29
|
"prepack": "npm run build",
|
|
27
30
|
"prepare": "husky",
|
|
28
|
-
"test": "
|
|
31
|
+
"test:unit": "vitest run",
|
|
32
|
+
"test:unit:coverage": "vitest run --coverage",
|
|
33
|
+
"test:unit:coverage:staged": "vitest related --run --coverage --coverage.changed --coverage.thresholds.perFile --coverage.thresholds.lines=80 --coverage.thresholds.functions=80 --coverage.thresholds.branches=80 --coverage.thresholds.statements=80 --coverage.thresholds.autoUpdate=false",
|
|
29
34
|
"typecheck": "tsc --noEmit"
|
|
30
35
|
},
|
|
31
36
|
"lint-staged": {
|
|
32
|
-
"!(*.ts|*.mts|*.cts|*.tsx|*.js|*.cjs|*.mjs|*.jsx)": "
|
|
33
|
-
"*.ts|*.mts|*.cts|*.tsx|*.js|*.cjs|*.mjs|*.jsx":
|
|
37
|
+
"!(*.ts|*.mts|*.cts|*.tsx|*.js|*.cjs|*.mjs|*.jsx)": "npm run format:staged --",
|
|
38
|
+
"*.ts|*.mts|*.cts|*.tsx|*.js|*.cjs|*.mjs|*.jsx": [
|
|
39
|
+
"npm run lint:staged --",
|
|
40
|
+
"npm run test:unit:coverage:staged --"
|
|
41
|
+
]
|
|
34
42
|
},
|
|
35
43
|
"dependencies": {
|
|
36
44
|
"@typescript-eslint/parser": "8.68.0",
|
|
@@ -45,6 +53,7 @@
|
|
|
45
53
|
"@types/estree": "1.0.9",
|
|
46
54
|
"@types/mdast": "4.0.4",
|
|
47
55
|
"@types/node": "26.4.0",
|
|
56
|
+
"@vitest/coverage-v8": "5.0.0",
|
|
48
57
|
"eslint": "10.9.1",
|
|
49
58
|
"husky": "9.1.7",
|
|
50
59
|
"lint-staged": "17.4.1",
|
|
@@ -52,7 +61,8 @@
|
|
|
52
61
|
"tsup": "8.5.1",
|
|
53
62
|
"tsx": "4.23.12",
|
|
54
63
|
"typescript": "6.0.3",
|
|
55
|
-
"
|
|
64
|
+
"vitest": "5.0.0",
|
|
65
|
+
"zirka": "0.0.65",
|
|
56
66
|
"zod": "4.4.3"
|
|
57
67
|
},
|
|
58
68
|
"peerDependencies": {
|