@akanjs/devkit 2.3.11-rc.8 → 2.3.11-rc.9

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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/qualityScanner.ts +91 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "2.3.11-rc.8",
3
+ "version": "2.3.11-rc.9",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -32,7 +32,7 @@
32
32
  "@langchain/openai": "^1.4.6",
33
33
  "@tailwindcss/node": "^4.3.0",
34
34
  "@trapezedev/project": "^7.1.4",
35
- "akanjs": "2.3.11-rc.8",
35
+ "akanjs": "2.3.11-rc.9",
36
36
  "chalk": "^5.6.2",
37
37
  "commander": "^14.0.3",
38
38
  "daisyui": "5.5.23",
package/qualityScanner.ts CHANGED
@@ -112,6 +112,7 @@ export class AkanQualityScanner {
112
112
  const warnings = [
113
113
  ...this.#scanGlobalQuality(sourceFiles),
114
114
  ...sourceFiles.flatMap((sourceFile) => this.#scanSingleFileQuality(sourceFile)),
115
+ ...sourceFiles.flatMap((sourceFile) => this.#scanComponentQuality(sourceFile)),
115
116
  ...sourceFiles.flatMap((sourceFile) => this.#scanConventionQuality(sourceFile)),
116
117
  ...sourceFiles.flatMap((sourceFile) => this.#scanLayoutQuality(sourceFile)),
117
118
  ];
@@ -256,6 +257,26 @@ export class AkanQualityScanner {
256
257
  return warnings;
257
258
  }
258
259
 
260
+ #scanComponentQuality(sourceFile: SourceFileInfo): QualityWarning[] {
261
+ if (!isComponentDeclarationFile(sourceFile.file)) return [];
262
+ const exportedNames = getExportedDeclarationNames(sourceFile.sourceFile);
263
+ const allowedInternalInterfaces = new Set([...exportedNames].map((name) => `${name}Props`));
264
+ const warnings: QualityWarning[] = [];
265
+ for (const declaration of getInternalTypeOrFunctionDeclarations(sourceFile.sourceFile)) {
266
+ if (exportedNames.has(declaration.name)) continue;
267
+ if (declaration.kind === "interface" && allowedInternalInterfaces.has(declaration.name)) continue;
268
+ warnings.push({
269
+ rule: "akan.file.component-internal-declaration",
270
+ scope: "file",
271
+ severity: "warning",
272
+ file: sourceFile.file,
273
+ line: declaration.line,
274
+ message: `Component file should not declare non-exported ${declaration.kind} "${declaration.name}". Only a "<Component>Props" interface may stay internal; move other types or helpers to a separate file in ui/, webkit/, or common/ by purpose.`,
275
+ });
276
+ }
277
+ return warnings;
278
+ }
279
+
259
280
  #scanConventionQuality(sourceFile: SourceFileInfo): QualityWarning[] {
260
281
  const suffix = CONVENTION_SUFFIXES.find((candidate) => sourceFile.file.endsWith(candidate));
261
282
  if (!suffix) return [];
@@ -347,7 +368,9 @@ function formatQualityLocation(file: string | undefined, line: number | undefine
347
368
 
348
369
  function getExportedFunctionLikes(sourceFile: SourceFileInfo): ExportedFunctionLike[] {
349
370
  const declarations: ExportedFunctionLike[] = [];
350
- const pageExempt = isPageRouteFile(sourceFile.file);
371
+ // UI component names (e.g. Card, Button) naturally repeat across apps/libs, so exempt ui files from the
372
+ // duplicate-exported-name check. This only relaxes the name check; the shared-body check still applies.
373
+ const nameExempt = isPageRouteFile(sourceFile.file) || isUiComponentFile(sourceFile.file);
351
374
  for (const statement of sourceFile.sourceFile.statements) {
352
375
  if (ts.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
353
376
  declarations.push({
@@ -356,7 +379,7 @@ function getExportedFunctionLikes(sourceFile: SourceFileInfo): ExportedFunctionL
356
379
  file: sourceFile.file,
357
380
  line: getLine(sourceFile.sourceFile, statement),
358
381
  bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, statement.body),
359
- duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile.file, false),
382
+ duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false),
360
383
  });
361
384
  }
362
385
  if (ts.isClassDeclaration(statement) && statement.name && isExported(statement)) {
@@ -367,7 +390,7 @@ function getExportedFunctionLikes(sourceFile: SourceFileInfo): ExportedFunctionL
367
390
  line: getLine(sourceFile.sourceFile, statement),
368
391
  bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, statement),
369
392
  duplicateNameExempt:
370
- pageExempt ||
393
+ nameExempt ||
371
394
  isConventionDuplicateNameExempt(sourceFile.file, isEnumClassStatement(sourceFile.sourceFile, statement)),
372
395
  });
373
396
  }
@@ -380,7 +403,7 @@ function getExportedFunctionLikes(sourceFile: SourceFileInfo): ExportedFunctionL
380
403
  file: sourceFile.file,
381
404
  line: getLine(sourceFile.sourceFile, declaration),
382
405
  bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, declaration.initializer),
383
- duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile.file, false),
406
+ duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false),
384
407
  });
385
408
  }
386
409
  }
@@ -393,6 +416,11 @@ function isPageRouteFile(file: string) {
393
416
  return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
394
417
  }
395
418
 
419
+ function isUiComponentFile(file: string) {
420
+ const segments = file.split("/");
421
+ return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "ui";
422
+ }
423
+
396
424
  function isConventionDuplicateNameExempt(file: string, isEnumClass: boolean) {
397
425
  if (!isInLibModule(file)) return false;
398
426
  if (file.endsWith(".tsx")) return true;
@@ -427,6 +455,65 @@ function getExportedClassNames(sourceFile: ts.SourceFile) {
427
455
  .map((statement) => statement.name!.text);
428
456
  }
429
457
 
458
+ function isComponentDeclarationFile(file: string) {
459
+ if (!file.endsWith(".tsx")) return false;
460
+ const segments = file.split("/");
461
+ const [root, , area] = segments;
462
+ if (root !== "apps" && root !== "libs") return false;
463
+ if (area === "lib" || area === "ui") return true;
464
+ return root === "apps" && area === "page";
465
+ }
466
+
467
+ function getExportedDeclarationNames(sourceFile: ts.SourceFile) {
468
+ const names = new Set<string>();
469
+ for (const statement of sourceFile.statements) {
470
+ if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
471
+ for (const element of statement.exportClause.elements) {
472
+ names.add(element.name.text);
473
+ if (element.propertyName) names.add(element.propertyName.text);
474
+ }
475
+ continue;
476
+ }
477
+ if (!isExported(statement)) continue;
478
+ for (const name of getStatementDeclarationNames(statement)) names.add(name);
479
+ }
480
+ return names;
481
+ }
482
+
483
+ function getStatementDeclarationNames(statement: ts.Statement): string[] {
484
+ if (ts.isFunctionDeclaration(statement) && statement.name) return [statement.name.text];
485
+ if (ts.isClassDeclaration(statement) && statement.name) return [statement.name.text];
486
+ if (ts.isInterfaceDeclaration(statement)) return [statement.name.text];
487
+ if (ts.isTypeAliasDeclaration(statement)) return [statement.name.text];
488
+ if (ts.isEnumDeclaration(statement)) return [statement.name.text];
489
+ if (ts.isVariableStatement(statement)) {
490
+ return statement.declarationList.declarations
491
+ .filter((declaration) => ts.isIdentifier(declaration.name))
492
+ .map((declaration) => (declaration.name as ts.Identifier).text);
493
+ }
494
+ return [];
495
+ }
496
+
497
+ function getInternalTypeOrFunctionDeclarations(sourceFile: ts.SourceFile) {
498
+ const declarations: Array<{ name: string; kind: "interface" | "type" | "function"; line: number }> = [];
499
+ for (const statement of sourceFile.statements) {
500
+ if (isExported(statement)) continue;
501
+ if (ts.isInterfaceDeclaration(statement)) {
502
+ declarations.push({ name: statement.name.text, kind: "interface", line: getLine(sourceFile, statement) });
503
+ } else if (ts.isTypeAliasDeclaration(statement)) {
504
+ declarations.push({ name: statement.name.text, kind: "type", line: getLine(sourceFile, statement) });
505
+ } else if (ts.isFunctionDeclaration(statement) && statement.name) {
506
+ declarations.push({ name: statement.name.text, kind: "function", line: getLine(sourceFile, statement) });
507
+ } else if (ts.isVariableStatement(statement)) {
508
+ for (const declaration of statement.declarationList.declarations) {
509
+ if (!ts.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer)) continue;
510
+ declarations.push({ name: declaration.name.text, kind: "function", line: getLine(sourceFile, declaration) });
511
+ }
512
+ }
513
+ }
514
+ return declarations;
515
+ }
516
+
430
517
  function getPlaceholderExportWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
431
518
  if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx")) return [];
432
519
  return getTopLevelDeclarations(sourceFile)