pasika 0.10.23 → 0.10.25
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/dist/eslint/pasika/index.d.ts +1 -0
- package/dist/eslint/pasika/index.js +148 -18
- package/package.json +1 -1
|
@@ -196,6 +196,7 @@ declare const pasikaPlugin: {
|
|
|
196
196
|
"guide-section-shape": _eslint_markdown.MarkdownRuleDefinition;
|
|
197
197
|
"filename-case": eslint.Rule.RuleModule;
|
|
198
198
|
"import-boundaries": eslint.Rule.RuleModule;
|
|
199
|
+
"import-spacing": eslint.Rule.RuleModule;
|
|
199
200
|
"named-exports": eslint.Rule.RuleModule;
|
|
200
201
|
"support-file-placement": eslint.Rule.RuleModule;
|
|
201
202
|
"support-folder-shape": eslint.Rule.RuleModule;
|
|
@@ -264,6 +264,86 @@ function findSimpleRoot(component, _text, _filename) {
|
|
|
264
264
|
if (roots.some((root) => root.tagName !== firstTag)) return void 0;
|
|
265
265
|
return roots[0];
|
|
266
266
|
}
|
|
267
|
+
function literalTestIds(attributes) {
|
|
268
|
+
return attributes.flatMap((candidate) => {
|
|
269
|
+
if (!ts.isJsxAttribute(candidate) || !ts.isIdentifier(candidate.name) || candidate.name.text !== "data-testid") {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
return candidate.initializer && ts.isStringLiteral(candidate.initializer) ? [candidate.initializer.text] : [];
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
function combineTestIdPaths(left, right) {
|
|
276
|
+
return left.flatMap((leftPath) => right.map((rightPath) => [...leftPath, ...rightPath]));
|
|
277
|
+
}
|
|
278
|
+
function renderedTestIdPathsFromExpression(expression) {
|
|
279
|
+
let unwrapped = expression;
|
|
280
|
+
while (ts.isParenthesizedExpression(unwrapped)) unwrapped = unwrapped.expression;
|
|
281
|
+
if (ts.isConditionalExpression(unwrapped)) {
|
|
282
|
+
return [
|
|
283
|
+
...renderedTestIdPathsFromExpression(unwrapped.whenTrue),
|
|
284
|
+
...renderedTestIdPathsFromExpression(unwrapped.whenFalse)
|
|
285
|
+
];
|
|
286
|
+
}
|
|
287
|
+
if (ts.isJsxSelfClosingElement(unwrapped)) {
|
|
288
|
+
return [literalTestIds(unwrapped.attributes.properties)];
|
|
289
|
+
}
|
|
290
|
+
if (ts.isJsxElement(unwrapped)) {
|
|
291
|
+
let paths = [literalTestIds(unwrapped.openingElement.attributes.properties)];
|
|
292
|
+
for (const child of unwrapped.children) {
|
|
293
|
+
if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child) || ts.isJsxFragment(child)) {
|
|
294
|
+
paths = combineTestIdPaths(paths, renderedTestIdPathsFromExpression(child));
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (ts.isJsxExpression(child) && child.expression) {
|
|
298
|
+
const childExpression = child.expression;
|
|
299
|
+
if (ts.isConditionalExpression(childExpression) || ts.isJsxElement(childExpression) || ts.isJsxSelfClosingElement(childExpression) || ts.isJsxFragment(childExpression) || ts.isParenthesizedExpression(childExpression)) {
|
|
300
|
+
paths = combineTestIdPaths(paths, renderedTestIdPathsFromExpression(childExpression));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return paths;
|
|
305
|
+
}
|
|
306
|
+
if (ts.isJsxFragment(unwrapped)) {
|
|
307
|
+
let paths = [[]];
|
|
308
|
+
for (const child of unwrapped.children) {
|
|
309
|
+
if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child) || ts.isJsxFragment(child)) {
|
|
310
|
+
paths = combineTestIdPaths(paths, renderedTestIdPathsFromExpression(child));
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (ts.isJsxExpression(child) && child.expression) {
|
|
314
|
+
const childExpression = child.expression;
|
|
315
|
+
if (ts.isConditionalExpression(childExpression) || ts.isJsxElement(childExpression) || ts.isJsxSelfClosingElement(childExpression) || ts.isJsxFragment(childExpression) || ts.isParenthesizedExpression(childExpression)) {
|
|
316
|
+
paths = combineTestIdPaths(paths, renderedTestIdPathsFromExpression(childExpression));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return paths;
|
|
321
|
+
}
|
|
322
|
+
return [[]];
|
|
323
|
+
}
|
|
324
|
+
function findRenderedTestIdPaths(component) {
|
|
325
|
+
const declaration = component.declaration;
|
|
326
|
+
let body;
|
|
327
|
+
if (ts.isFunctionDeclaration(declaration)) body = declaration.body;
|
|
328
|
+
else if (declaration.initializer && ts.isArrowFunction(declaration.initializer)) body = declaration.initializer.body;
|
|
329
|
+
else if (declaration.initializer && ts.isFunctionExpression(declaration.initializer)) {
|
|
330
|
+
body = declaration.initializer.body;
|
|
331
|
+
}
|
|
332
|
+
if (!body) return [];
|
|
333
|
+
if (!ts.isBlock(body)) return renderedTestIdPathsFromExpression(body);
|
|
334
|
+
const returns = [];
|
|
335
|
+
const visit = (node) => {
|
|
336
|
+
if (node !== body && (ts.isFunctionLike(node) || ts.isClassLike(node))) return;
|
|
337
|
+
if (ts.isReturnStatement(node)) returns.push(node);
|
|
338
|
+
ts.forEachChild(node, visit);
|
|
339
|
+
};
|
|
340
|
+
visit(body);
|
|
341
|
+
return returns.flatMap((statement) => {
|
|
342
|
+
const expression = statement.expression;
|
|
343
|
+
if (!expression || expression.kind === ts.SyntaxKind.NullKeyword) return [];
|
|
344
|
+
return renderedTestIdPathsFromExpression(expression);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
267
347
|
function getTestId(root) {
|
|
268
348
|
const attribute = root.attributes.find(
|
|
269
349
|
(candidate) => ts.isJsxAttribute(candidate) && ts.isIdentifier(candidate.name) && candidate.name.text === "data-testid"
|
|
@@ -507,6 +587,50 @@ var importBoundariesRule = {
|
|
|
507
587
|
}
|
|
508
588
|
};
|
|
509
589
|
|
|
590
|
+
// eslint/rules/import-spacing.ts
|
|
591
|
+
var MESSAGE = "Consecutive import declarations must not be separated by a blank line. See docs/next-codebase-guide/rules/exports-and-imports-rule.md";
|
|
592
|
+
function hasBlankLine(text) {
|
|
593
|
+
return /\r?\n[\t ]*\r?\n/u.test(text);
|
|
594
|
+
}
|
|
595
|
+
function removeBlankLines(text) {
|
|
596
|
+
const newline = text.includes("\r\n") ? "\r\n" : "\n";
|
|
597
|
+
return text.replace(/(?:\r?\n[\t ]*){2,}/gu, newline);
|
|
598
|
+
}
|
|
599
|
+
var importSpacingRule = {
|
|
600
|
+
meta: {
|
|
601
|
+
schema: [],
|
|
602
|
+
type: "layout",
|
|
603
|
+
fixable: "whitespace",
|
|
604
|
+
docs: {
|
|
605
|
+
description: "Disallow blank lines between consecutive import declarations."
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
create(context) {
|
|
609
|
+
return {
|
|
610
|
+
Program() {
|
|
611
|
+
const statements = context.sourceCode.ast.body;
|
|
612
|
+
for (let index = 1; index < statements.length; index += 1) {
|
|
613
|
+
const previous = statements[index - 1];
|
|
614
|
+
const current = statements[index];
|
|
615
|
+
if (previous?.type !== "ImportDeclaration" || current?.type !== "ImportDeclaration") continue;
|
|
616
|
+
const previousEnd = previous.range?.[1];
|
|
617
|
+
const currentStart = current.range?.[0];
|
|
618
|
+
if (previousEnd === void 0 || currentStart === void 0) continue;
|
|
619
|
+
const gap = context.sourceCode.text.slice(previousEnd, currentStart);
|
|
620
|
+
if (!hasBlankLine(gap)) continue;
|
|
621
|
+
context.report({
|
|
622
|
+
node: current,
|
|
623
|
+
message: MESSAGE,
|
|
624
|
+
fix(fixer) {
|
|
625
|
+
return fixer.replaceTextRange([previousEnd, currentStart], removeBlankLines(gap));
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
|
|
510
634
|
// eslint/rules/no-mixed-concerns.ts
|
|
511
635
|
var noMixedConcernsRule = {
|
|
512
636
|
meta: {
|
|
@@ -2193,7 +2317,6 @@ var NEXT_ROUTING_FILES2 = /* @__PURE__ */ new Set([
|
|
|
2193
2317
|
"sitemap",
|
|
2194
2318
|
"twitter-image"
|
|
2195
2319
|
]);
|
|
2196
|
-
var isPascalCase5 = (value) => /^[A-Z][A-Za-z0-9]*$/.test(value);
|
|
2197
2320
|
var isKebabCase3 = (value) => /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value);
|
|
2198
2321
|
var dataTestIdCaseRule = {
|
|
2199
2322
|
meta: {
|
|
@@ -2213,28 +2336,34 @@ var dataTestIdCaseRule = {
|
|
|
2213
2336
|
return {
|
|
2214
2337
|
Program(node) {
|
|
2215
2338
|
for (const component of components) {
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
const
|
|
2219
|
-
const
|
|
2220
|
-
if (
|
|
2339
|
+
if (component.smart) {
|
|
2340
|
+
const expected2 = component.name;
|
|
2341
|
+
const renderedPaths = findRenderedTestIdPaths(component);
|
|
2342
|
+
const hasStableAnchor = renderedPaths.length > 0 && renderedPaths.every((values) => values.filter((value2) => value2 === expected2).length === 1);
|
|
2343
|
+
if (hasStableAnchor) continue;
|
|
2344
|
+
const hasSingleWrongCasedAnchor = renderedPaths.length > 0 && renderedPaths.every((values) => values.length === 1 && values[0] !== expected2) && new Set(renderedPaths.map((values) => values[0])).size === 1;
|
|
2345
|
+
if (hasSingleWrongCasedAnchor) {
|
|
2221
2346
|
context.report({
|
|
2222
2347
|
node,
|
|
2223
|
-
message: `
|
|
2348
|
+
message: `data-testid for smart component "${component.name}" must be PascalCase: expected "${expected2}".`
|
|
2224
2349
|
});
|
|
2225
2350
|
continue;
|
|
2226
2351
|
}
|
|
2227
|
-
if (value === void 0 || value === expected) continue;
|
|
2228
|
-
if (component.smart ? !isPascalCase5(value) || value !== expected : !isKebabCase3(value)) {
|
|
2229
|
-
context.report({
|
|
2230
|
-
node,
|
|
2231
|
-
message: `data-testid for ${component.smart ? "smart" : "dumb"} component "${component.name}" must be ${component.smart ? "PascalCase" : "kebab-case"}: expected "${expected}".`
|
|
2232
|
-
});
|
|
2233
|
-
}
|
|
2234
|
-
} else if (component.smart) {
|
|
2235
2352
|
context.report({
|
|
2236
2353
|
node,
|
|
2237
|
-
message: `Smart component "${component.name}"
|
|
2354
|
+
message: `Smart component "${component.name}" must expose exactly one stable data-testid="${expected2}" anchor for itself in every rendered result. Place it on the existing DOM surface or on a child component that forwards data-testid instead of adding an artificial wrapper. See docs/next-codebase-guide/rules/smart-vs-dumb-component-rule.md`
|
|
2355
|
+
});
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2358
|
+
const root = findSimpleRoot(component, text, filename);
|
|
2359
|
+
if (!root) continue;
|
|
2360
|
+
const { value } = getTestId(root);
|
|
2361
|
+
const expected = toKebabCase(component.name);
|
|
2362
|
+
if (value === void 0 || value === expected) continue;
|
|
2363
|
+
if (!isKebabCase3(value)) {
|
|
2364
|
+
context.report({
|
|
2365
|
+
node,
|
|
2366
|
+
message: `data-testid for dumb component "${component.name}" must be kebab-case: expected "${expected}".`
|
|
2238
2367
|
});
|
|
2239
2368
|
}
|
|
2240
2369
|
}
|
|
@@ -4837,7 +4966,7 @@ var schemaCasingRule = {
|
|
|
4837
4966
|
|
|
4838
4967
|
// eslint/rules/component-casing.ts
|
|
4839
4968
|
import path42 from "path";
|
|
4840
|
-
function
|
|
4969
|
+
function isPascalCase5(name) {
|
|
4841
4970
|
return /^[A-Z][A-Za-z0-9]*$/.test(name);
|
|
4842
4971
|
}
|
|
4843
4972
|
var componentCasingRule = {
|
|
@@ -4856,7 +4985,7 @@ var componentCasingRule = {
|
|
|
4856
4985
|
return {
|
|
4857
4986
|
Program(node) {
|
|
4858
4987
|
for (const declaration of findJsxReturningDeclarations(context.sourceCode.text, filename)) {
|
|
4859
|
-
if (
|
|
4988
|
+
if (isPascalCase5(declaration.name)) continue;
|
|
4860
4989
|
context.report({
|
|
4861
4990
|
node,
|
|
4862
4991
|
loc: { line: declaration.line, column: declaration.column },
|
|
@@ -7643,6 +7772,7 @@ var pasikaNextjsAppRules = {
|
|
|
7643
7772
|
// Framework-agnostic TypeScript rules.
|
|
7644
7773
|
"filename-case": filenameCaseRule,
|
|
7645
7774
|
"import-boundaries": importBoundariesRule,
|
|
7775
|
+
"import-spacing": importSpacingRule,
|
|
7646
7776
|
"named-exports": namedExportsRule,
|
|
7647
7777
|
"support-file-placement": supportFilePlacementRule,
|
|
7648
7778
|
"support-folder-shape": supportFolderShapeRule,
|