@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.
- package/package.json +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.
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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)
|