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.
@@ -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
- const root = findSimpleRoot(component, text, filename);
2217
- if (root) {
2218
- const { value } = getTestId(root);
2219
- const expected = component.smart ? component.name : toKebabCase(component.name);
2220
- if (component.smart && value === void 0) {
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: `Smart component "${component.name}" with one outer DOM element must set data-testid="${expected}".`
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}" has no single outer element; wrap its content in one outer element with data-testid="${component.name}" instead of rendering multiple roots. See docs/next-codebase-guide/rules/smart-vs-dumb-component-rule.md`
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 isPascalCase6(name) {
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 (isPascalCase6(declaration.name)) continue;
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pasika",
3
- "version": "0.10.23",
3
+ "version": "0.10.25",
4
4
  "description": "Reusable agent setup package",
5
5
  "repository": {
6
6
  "type": "git",