@akanjs/cli 3.0.0-beta.8 → 3.0.0
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/.build-stamp +1 -1
- package/CodeAgent-2vmp9qe0.js +9 -0
- package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
- package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
- package/CodeTui-5ed6bg2a.js +1457 -0
- package/README.ko.md +83 -28
- package/README.md +70 -27
- package/TunnelShare-cwh9ezsm.js +9 -0
- package/{agent.command-0q17pnrs.js → agent.command-gn73rhe7.js} +8 -9
- package/akanCode.js +102 -0
- package/application.command-e968t0hw.js +246 -0
- package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-64taxseg.js} +121 -77
- package/buildBatch.proc.js +120 -32
- package/cloud.command-0jm301rr.js +95 -0
- package/cloud.script-nemnzjpv.js +20 -0
- package/code.command-18kcm7hq.js +154 -0
- package/commandManifest.json +1 -1
- package/consoleToStderr-h5bryj94.js +7 -0
- package/context.command-trd9bn18.js +48 -0
- package/context.runner-y5hwh673.js +21 -0
- package/{dependencyScanner-shs82ynp.js → dependencyScanner-06z2dmrr.js} +2 -2
- package/{devTui-m1atgkc9.js → devTui-q7k7bakd.js} +122 -40
- package/fileSys-wzzrntkw.js +9 -0
- package/guideline.command-bdmdn2de.js +42 -0
- package/guidelines/agentRule/agentRule.instruction.md +154 -26
- package/guidelines/conventions/conventions.instruction.md +134 -48
- package/guidelines/mcpRule/mcpRule.instruction.md +7 -5
- package/guidelines/queryRule/queryRule.instruction.md +21 -7
- package/guidelines/runtimeRule/runtimeRule.instruction.md +291 -6
- package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
- package/guidelines/transportRule/transportRule.instruction.md +10 -8
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +20 -0
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +66 -3
- package/incrementalBuilder.proc.js +564 -109
- package/index-0wpybxtr.js +2729 -0
- package/{index-kcxmv48c.js → index-2ahj896a.js} +45 -79
- package/index-2wc41k5w.js +49 -0
- package/index-3hx1z96c.js +87 -0
- package/{index-2tv3fdx4.js → index-3w94e7n3.js} +1228 -1458
- package/{index-hwkygn9s.js → index-48k96p55.js} +325 -36
- package/index-4npz05nm.js +187 -0
- package/{index-q0y2jq9j.js → index-52w0fvh8.js} +21 -4
- package/index-6he6gvfg.js +4300 -0
- package/index-7md2g9vw.js +6 -0
- package/{index-04bk599g.js → index-93j60em0.js} +10 -17
- package/index-94s080qm.js +266 -0
- package/index-9dp031by.js +12 -0
- package/{index-ktb5njgy.js → index-9z771ptz.js} +49 -99
- package/{index-y6n8nmny.js → index-abxpehf8.js} +75 -203
- package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
- package/{index-9077wzd2.js → index-by9pxkr0.js} +43 -26
- package/index-c6eggbee.js +123 -0
- package/index-d2242ksy.js +99 -0
- package/index-drehwpdt.js +1119 -0
- package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
- package/{index-ffvk10zz.js → index-f3c78nd6.js} +107 -241
- package/index-gbggbqcp.js +1058 -0
- package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
- package/index-hsmtysz4.js +53 -0
- package/{index-j9yhg404.js → index-jmrn54tt.js} +4 -7
- package/{index-a64esmtb.js → index-kcdj1z5v.js} +523 -1022
- package/{index-d43r08tf.js → index-kqabytj7.js} +77 -150
- package/{index-vyvre9en.js → index-mbxbhay5.js} +6 -2
- package/{index-vxtad5bd.js → index-mgxpx3f2.js} +5 -8
- package/index-p6pbz9n5.js +25 -0
- package/{index-sj5vxr8f.js → index-p993efkq.js} +981 -897
- package/{index-2xp0kfkt.js → index-pmhqttv3.js} +37 -25
- package/index-tqm7jjcn.js +700 -0
- package/{index-0g7r8xz3.js → index-vpj42xwt.js} +114 -232
- package/index-wa5jbxj1.js +760 -0
- package/{index-k2j1hc4r.js → index-wprnsjms.js} +12 -12
- package/{index-gyd220p2.js → index-y3mphr1m.js} +554 -577
- package/index-yg7jhz8x.js +69 -0
- package/{index-h7yw2z9n.js → index-zzvvc845.js} +8 -14
- package/index.js +23 -22
- package/{library.command-qzzbj6d8.js → library.command-9h749a1x.js} +4 -5
- package/{localRegistry.command-vyrgg4xw.js → localRegistry.command-2bqqhxyz.js} +23 -43
- package/{module.command-rq5hq3ax.js → module.command-nv8nzgft.js} +8 -12
- package/{package.command-d9hd6v9n.js → package.command-mq4bggp6.js} +6 -7
- package/package.json +15 -7
- package/{page.command-126d56te.js → page.command-j639n707.js} +4 -5
- package/{primitive.command-taxd1538.js → primitive.command-kz6849th.js} +9 -23
- package/prompter-76k56b37.js +8 -0
- package/{quality.command-6gsna0a3.js → quality.command-4hyabwkb.js} +205 -377
- package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
- package/{repair.command-tq2dgy72.js → repair.command-a9awpbqr.js} +11 -35
- package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
- package/{scalar.command-3bcmrrkv.js → scalar.command-ma4fpm9m.js} +7 -11
- package/skills/akan-code/SKILL.md +130 -0
- package/skills/akan-debug/SKILL.md +42 -0
- package/skills/akan-deps/SKILL.md +50 -0
- package/skills/akan-err/SKILL.md +39 -0
- package/skills/akan-module/SKILL.md +45 -0
- package/skills/akan-scaffold/SKILL.md +76 -0
- package/skills/akan-scalar/SKILL.md +52 -0
- package/skills/akan-service/SKILL.md +34 -0
- package/skills/akan-store/SKILL.md +67 -0
- package/skills/akan-ui/SKILL.md +63 -0
- package/skills/akan-validate/SKILL.md +44 -0
- package/{subspace.command-eddzqpx3.js → subspace.command-3k2pmrs4.js} +53 -62
- package/templates/__scalar/__model__/__model__.abstract.md.template +8 -0
- package/templates/app/page/_index.tsx +257 -258
- package/templates/app/page/_layout.tsx +5 -1
- package/templates/app/page/_overrides.tsx +12 -0
- package/templates/app/page/styles.css.template +316 -14
- package/templates/app/public/apple-touch-icon.png +0 -0
- package/templates/app/public/favicon.ico +0 -0
- package/templates/app/public/jelly/cloud.webp +0 -0
- package/templates/app/public/jelly/comet.webp +0 -0
- package/templates/app/public/jelly/moon.webp +0 -0
- package/templates/app/public/jelly/planet.webp +0 -0
- package/templates/app/public/jelly/rocket.webp +0 -0
- package/templates/app/public/jelly/star.webp +0 -0
- package/templates/app/public/jelly/sugar.webp +0 -0
- package/templates/app/public/logo.png +0 -0
- package/templates/app/ui/Recipe/index.ts +9 -0
- package/templates/app/ui/Recipe/jellyButton.ts +63 -0
- package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +8 -39
- package/templates/appSample/lib/_noti/noti.abstract.ts +6 -29
- package/templates/appSample/lib/task/Task.Unit.tsx +1 -1
- package/templates/appSample/lib/task/Task.Util.tsx +10 -13
- package/templates/appSample/lib/task/Task.View.tsx +2 -2
- package/templates/appSample/lib/task/Task.Zone.tsx +5 -2
- package/templates/appSample/lib/task/task.abstract.ts +12 -32
- package/templates/appSample/lib/task/task.signal.spec.ts +1 -15
- package/templates/appSample/lib/task/task.signal.test.ts +15 -1
- package/templates/appSample/lib/task/task.signal.ts +10 -8
- package/templates/appSample/page/task/[taskId]/_index.tsx +6 -6
- package/templates/appSample/page/task/[taskId]/edit.tsx +5 -4
- package/templates/appSample/page/task/_index.tsx +4 -3
- package/templates/appSample/page/task/_layout.tsx +1 -1
- package/templates/appSample/page/task/new.tsx +5 -4
- package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +7 -4
- package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +6 -4
- package/templates/appSample/ui/GlobalLoading.tsx +2 -1
- package/templates/appSample/ui/QuantityControl.tsx +3 -3
- package/templates/client.ts +7 -30
- package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -1
- package/templates/crudPages/[__model__Id]/page.tsx +2 -1
- package/templates/crudPages/new/page.tsx +2 -1
- package/templates/crudPages/page.tsx +2 -1
- package/templates/crudSinglePage/page.tsx +2 -1
- package/templates/lib/__lib/lib.dictionary.ts +0 -2
- package/templates/lib/__lib/lib.document.ts +0 -2
- package/templates/lib/sig.ts +8 -30
- package/templates/localDev/docker-compose.yaml.template +3 -14
- package/templates/module/__model__.signal.ts +12 -4
- package/templates/pkgRoot/tsconfig.json.template +2 -7
- package/templates/server.ts +1 -3
- package/templates/service/__model__.abstract.md.template +8 -0
- package/templates/workspaceRoot/.gitignore.template +0 -1
- package/templates/workspaceRoot/AGENTS.md.template +6 -0
- package/templates/workspaceRoot/package.json.template +1 -3
- package/tunnel.command-xwr46b66.js +129 -0
- package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
- package/typecheck.proc.js +1 -1
- package/workflow.command-t7wqe4h7.js +79 -0
- package/{workspace.command-06wvstgr.js → workspace.command-4derhzrh.js} +79 -109
- package/abstractCompactor-gmkc0z8j.js +0 -102
- package/application.command-18y0s485.js +0 -204
- package/applicationReleasePackager-4yasxwv9.js +0 -244
- package/capacitorApp-36rbr858.js +0 -12
- package/cloud.command-av97ndh6.js +0 -94
- package/cloud.script-cm11xjdz.js +0 -20
- package/context.command-5v37ef3n.js +0 -81
- package/devTuiView-0hc2emhe.js +0 -9
- package/getDirname-dr3jz1xg.js +0 -7
- package/guideline.command-4pz9g1rt.js +0 -360
- package/guidelines/agentRule/agentRule.generate.json +0 -31
- package/guidelines/componentRule/componentRule.generate.json +0 -31
- package/guidelines/conventions/conventions.generate.json +0 -25
- package/guidelines/cssRule/cssRule.generate.json +0 -31
- package/guidelines/docPageRule/docPageRule.generate.json +0 -31
- package/guidelines/docPageRule/docPageRule.instruction.md +0 -25
- package/guidelines/docSyncRule/docSyncRule.generate.json +0 -31
- package/guidelines/docSyncRule/docSyncRule.instruction.md +0 -25
- package/guidelines/enumConstant/enumConstant.generate.json +0 -31
- package/guidelines/enumConstant/enumConstant.instruction.md +0 -28
- package/guidelines/fieldRule/fieldRule.generate.json +0 -31
- package/guidelines/framework/framework.generate.json +0 -32
- package/guidelines/mcpRule/mcpRule.generate.json +0 -35
- package/guidelines/modelConstant/modelConstant.generate.json +0 -32
- package/guidelines/modelDictionary/modelDictionary.generate.json +0 -32
- package/guidelines/modelDocument/modelDocument.generate.json +0 -31
- package/guidelines/modelService/modelService.generate.json +0 -31
- package/guidelines/modelSignal/modelSignal.generate.json +0 -31
- package/guidelines/modelStore/modelStore.generate.json +0 -31
- package/guidelines/modelTemplate/modelTemplate.generate.json +0 -31
- package/guidelines/modelUnit/modelUnit.generate.json +0 -31
- package/guidelines/modelUtil/modelUtil.generate.json +0 -31
- package/guidelines/modelView/modelView.generate.json +0 -31
- package/guidelines/modelZone/modelZone.generate.json +0 -31
- package/guidelines/moduleCodegen/moduleCodegen.generate.json +0 -31
- package/guidelines/moduleCodegen/moduleCodegen.instruction.md +0 -28
- package/guidelines/moduleOverview/moduleOverview.generate.json +0 -31
- package/guidelines/queryRule/queryRule.generate.json +0 -31
- package/guidelines/recipeRule/recipeRule.generate.json +0 -31
- package/guidelines/runtimeRule/runtimeRule.generate.json +0 -25
- package/guidelines/scalarConstant/scalarConstant.generate.json +0 -32
- package/guidelines/scalarDictionary/scalarDictionary.generate.json +0 -32
- package/guidelines/scalarModule/scalarModule.generate.json +0 -31
- package/guidelines/sharedUiUsage/sharedUiUsage.generate.json +0 -31
- package/guidelines/sharedUiUsage/sharedUiUsage.instruction.md +0 -25
- package/guidelines/ssrRule/ssrRule.generate.json +0 -31
- package/guidelines/transportRule/transportRule.generate.json +0 -31
- package/guidelines/utilUiUsage/utilUiUsage.generate.json +0 -31
- package/guidelines/utilUiUsage/utilUiUsage.instruction.md +0 -25
- package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +0 -24
- package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +0 -33
- package/index-47pj3aee.js +0 -76
- package/index-5ystnsd5.js +0 -841
- package/index-6wwzs1bc.js +0 -7
- package/index-a17x49vg.js +0 -229
- package/index-m0xccmmm.js +0 -228
- package/index-meh558jy.js +0 -65
- package/index-rtxxan88.js +0 -471
- package/index-te54x9zm.js +0 -338
- package/index-w8kmx1b6.js +0 -2176
- package/index-wt7qrtd4.js +0 -1516
- package/templates/__scalar/__model__/__model__.abstract.md +0 -28
- package/templates/module/__model__.abstract.md +0 -28
- package/templates/service/__model__.abstract.md +0 -28
- package/templates/workspaceRoot/patches/react-dom@19.2.7.patch.template +0 -62
- package/workflow.command-gtdnzvbz.js +0 -108
|
@@ -1,29 +1,89 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
Workspace,
|
|
4
|
-
runner,
|
|
5
4
|
script,
|
|
6
5
|
command
|
|
7
|
-
} from "./index-
|
|
8
|
-
import"./index-
|
|
6
|
+
} from "./index-vpj42xwt.js";
|
|
7
|
+
import"./index-52w0fvh8.js";
|
|
9
8
|
import {
|
|
10
9
|
rootAllowedFiles,
|
|
11
10
|
rootAllowedDirs,
|
|
12
|
-
isAllowedLibFacetRootFile
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
AbstractDoc
|
|
17
|
-
} from "./index-meh558jy.js";
|
|
11
|
+
isAllowedLibFacetRootFile,
|
|
12
|
+
rootEntryHintOf
|
|
13
|
+
} from "./index-3w94e7n3.js";
|
|
18
14
|
|
|
19
15
|
// pkgs/@akanjs/devkit/qualityScanner.ts
|
|
20
16
|
import { createHash } from "crypto";
|
|
21
17
|
import { readdir, readFile, stat } from "fs/promises";
|
|
22
|
-
import
|
|
18
|
+
import path2 from "path";
|
|
23
19
|
import { RESERVED_ROUTE_CONFIG_EXPORTS } from "akanjs/common";
|
|
24
20
|
import ignore from "ignore";
|
|
25
21
|
import ts3 from "typescript";
|
|
26
22
|
|
|
23
|
+
// pkgs/@akanjs/devkit/abstractDoc.ts
|
|
24
|
+
import path from "path";
|
|
25
|
+
|
|
26
|
+
class AbstractDoc {
|
|
27
|
+
path;
|
|
28
|
+
content;
|
|
29
|
+
siblingFiles;
|
|
30
|
+
static suffix = ".abstract.md";
|
|
31
|
+
static maxLines = 300;
|
|
32
|
+
static #facetRoots = "{lib,ui,webkit,srvkit,common,plugin}";
|
|
33
|
+
static isAbstractPath(filePath) {
|
|
34
|
+
return filePath.endsWith(AbstractDoc.suffix);
|
|
35
|
+
}
|
|
36
|
+
static lineCountOf(content) {
|
|
37
|
+
return content.split(/\r?\n/).length;
|
|
38
|
+
}
|
|
39
|
+
static kindOf(filePath) {
|
|
40
|
+
const [root, folder, ...rest] = filePath.split("/");
|
|
41
|
+
if (root !== "lib" || !folder)
|
|
42
|
+
return "other";
|
|
43
|
+
if (folder === "__scalar")
|
|
44
|
+
return rest.length === 2 ? "scalar" : "other";
|
|
45
|
+
if (rest.length !== 1)
|
|
46
|
+
return "other";
|
|
47
|
+
return folder.startsWith("_") ? "service" : "domain";
|
|
48
|
+
}
|
|
49
|
+
static async findAll(sys, { module } = {}) {
|
|
50
|
+
const filePaths = (await sys.getAllFiles(`${AbstractDoc.#facetRoots}/**/*${AbstractDoc.suffix}`)).map((filePath) => filePath.split(path.sep).join("/")).sort();
|
|
51
|
+
const docs = await Promise.all(filePaths.map((filePath) => AbstractDoc.read(sys, filePath)));
|
|
52
|
+
if (!module)
|
|
53
|
+
return docs;
|
|
54
|
+
return docs.filter((doc) => doc.moduleName === module || doc.folderName === module);
|
|
55
|
+
}
|
|
56
|
+
static async read(sys, filePath) {
|
|
57
|
+
const dirPath = filePath.split("/").slice(0, -1).join("/");
|
|
58
|
+
const [content, folderFiles] = await Promise.all([sys.readFile(filePath), sys.readdir(dirPath)]);
|
|
59
|
+
return new AbstractDoc(filePath, content, folderFiles.filter((fileName) => !AbstractDoc.isAbstractPath(fileName)));
|
|
60
|
+
}
|
|
61
|
+
constructor(path2, content, siblingFiles) {
|
|
62
|
+
this.path = path2;
|
|
63
|
+
this.content = content;
|
|
64
|
+
this.siblingFiles = siblingFiles;
|
|
65
|
+
}
|
|
66
|
+
get kind() {
|
|
67
|
+
return AbstractDoc.kindOf(this.path);
|
|
68
|
+
}
|
|
69
|
+
get folderName() {
|
|
70
|
+
return this.path.split("/").at(-2) ?? "";
|
|
71
|
+
}
|
|
72
|
+
get moduleName() {
|
|
73
|
+
return (this.path.split("/").at(-1) ?? "").slice(0, -AbstractDoc.suffix.length);
|
|
74
|
+
}
|
|
75
|
+
get lineCount() {
|
|
76
|
+
return AbstractDoc.lineCountOf(this.content);
|
|
77
|
+
}
|
|
78
|
+
canReplaceWith(candidate) {
|
|
79
|
+
const next = candidate.trim();
|
|
80
|
+
if (!next.startsWith("#"))
|
|
81
|
+
return false;
|
|
82
|
+
const lineCount = AbstractDoc.lineCountOf(next);
|
|
83
|
+
return lineCount >= 3 && lineCount < this.lineCount;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
27
87
|
// pkgs/@akanjs/devkit/formSetterScanner.ts
|
|
28
88
|
import ts from "typescript";
|
|
29
89
|
|
|
@@ -99,25 +159,11 @@ var STATIC_COMPONENT_MIN_MASS = 4;
|
|
|
99
159
|
var MIXED_COMPONENT_MIN_MASS = 10;
|
|
100
160
|
var MIXED_COMPONENT_MAX_TOUCHES = 2;
|
|
101
161
|
var MODULE_SERVER_VIEW_MIN_CLIENT_MASS = 12;
|
|
162
|
+
var fileWarning = (rule, scope, file, message, line) => line === undefined ? { rule, scope, severity: "warning", file, message } : { rule, scope, severity: "warning", file, line, message };
|
|
102
163
|
|
|
103
164
|
class SsrScanner {
|
|
104
165
|
static #serverSafeCalls = new Set(["usePage", "getSelf", "useServer"]);
|
|
105
|
-
static #clientGlobals = new Set(
|
|
106
|
-
"window",
|
|
107
|
-
"document",
|
|
108
|
-
"navigator",
|
|
109
|
-
"localStorage",
|
|
110
|
-
"sessionStorage",
|
|
111
|
-
"location",
|
|
112
|
-
"history",
|
|
113
|
-
"screen",
|
|
114
|
-
"matchMedia",
|
|
115
|
-
"IntersectionObserver",
|
|
116
|
-
"ResizeObserver",
|
|
117
|
-
"MutationObserver",
|
|
118
|
-
"requestAnimationFrame",
|
|
119
|
-
"WebSocket"
|
|
120
|
-
]);
|
|
166
|
+
static #clientGlobals = new Set("window document navigator localStorage sessionStorage location history screen matchMedia IntersectionObserver".split(" ").concat("ResizeObserver MutationObserver requestAnimationFrame WebSocket".split(" ")));
|
|
121
167
|
static #clientRuntimeImports = new Set(["st", "fetch"]);
|
|
122
168
|
scan(sourceFiles) {
|
|
123
169
|
const componentFiles = sourceFiles.filter((sourceFile) => this.#isBalancedFile(sourceFile.file));
|
|
@@ -138,38 +184,17 @@ class SsrScanner {
|
|
|
138
184
|
const components = this.#getComponents(sourceFile, vendorNames);
|
|
139
185
|
const warnings = [];
|
|
140
186
|
if (!hasVendorImport && !importsClientRuntime && this.#getTouches(sourceFile.sourceFile, sourceFile.sourceFile).length === 0 && !this.#isConventionClientFile(sourceFile.file)) {
|
|
141
|
-
warnings.push(
|
|
142
|
-
rule: "akan.ssr.unnecessary-use-client",
|
|
143
|
-
scope: "ssr",
|
|
144
|
-
severity: "warning",
|
|
145
|
-
file: sourceFile.file,
|
|
146
|
-
line: 1,
|
|
147
|
-
message: `"use client" is declared but the file uses no client-only capability (hook, event handler, store, or browser API).`
|
|
148
|
-
});
|
|
187
|
+
warnings.push(fileWarning("akan.ssr.unnecessary-use-client", "ssr", sourceFile.file, `"use client" is declared but the file uses no client-only capability (hook, event handler, store, or browser API).`, 1));
|
|
149
188
|
}
|
|
150
189
|
for (const component of components) {
|
|
151
190
|
if (component.vendorTags)
|
|
152
191
|
continue;
|
|
153
192
|
if (component.touches.length === 0 && component.mass >= STATIC_COMPONENT_MIN_MASS) {
|
|
154
|
-
warnings.push({
|
|
155
|
-
rule: "akan.ssr.client-static-component",
|
|
156
|
-
scope: "ssr",
|
|
157
|
-
severity: "warning",
|
|
158
|
-
file: sourceFile.file,
|
|
159
|
-
line: component.line,
|
|
160
|
-
message: `Client component "${component.name}" renders ${component.mass} JSX elements with no client-only capability. It is server-renderable markup sitting in the client bundle.`
|
|
161
|
-
});
|
|
193
|
+
warnings.push(fileWarning("akan.ssr.client-static-component", "ssr", sourceFile.file, `Client component "${component.name}" renders ${component.mass} JSX elements with no client-only capability. It is server-renderable markup sitting in the client bundle.`, component.line));
|
|
162
194
|
continue;
|
|
163
195
|
}
|
|
164
196
|
if (component.touches.length >= 1 && component.touches.length <= MIXED_COMPONENT_MAX_TOUCHES && component.mass >= MIXED_COMPONENT_MIN_MASS) {
|
|
165
|
-
warnings.push({
|
|
166
|
-
rule: "akan.ssr.client-static-markup",
|
|
167
|
-
scope: "ssr",
|
|
168
|
-
severity: "warning",
|
|
169
|
-
file: sourceFile.file,
|
|
170
|
-
line: component.line,
|
|
171
|
-
message: `Client component "${component.name}" renders ${component.mass} JSX elements around only ${component.touches.length} client-only touch (${[...new Set(component.touches)].join(", ")}). Most of this subtree does not need the client bundle.`
|
|
172
|
-
});
|
|
197
|
+
warnings.push(fileWarning("akan.ssr.client-static-markup", "ssr", sourceFile.file, `Client component "${component.name}" renders ${component.mass} JSX elements around only ${component.touches.length} client-only touch (${[...new Set(component.touches)].join(", ")}). Most of this subtree does not need the client bundle.`, component.line));
|
|
173
198
|
}
|
|
174
199
|
}
|
|
175
200
|
warnings.push(...this.#getMountLoadWarnings(sourceFile));
|
|
@@ -189,32 +214,16 @@ class SsrScanner {
|
|
|
189
214
|
entry.serverFiles += 1;
|
|
190
215
|
modules.set(moduleDir, entry);
|
|
191
216
|
}
|
|
192
|
-
return [...modules].filter(([, entry]) => entry.serverFiles === 0 && entry.clientMass >= MODULE_SERVER_VIEW_MIN_CLIENT_MASS).map(([moduleDir, entry]) => ({
|
|
193
|
-
rule: "akan.ssr.module-missing-server-view",
|
|
194
|
-
scope: "ssr",
|
|
195
|
-
severity: "warning",
|
|
196
|
-
file: entry.line,
|
|
197
|
-
message: `Module "${moduleDir}" renders ${entry.clientMass} JSX elements from client files only; it declares no Unit or View server component.`
|
|
198
|
-
}));
|
|
217
|
+
return [...modules].filter(([, entry]) => entry.serverFiles === 0 && entry.clientMass >= MODULE_SERVER_VIEW_MIN_CLIENT_MASS).map(([moduleDir, entry]) => fileWarning("akan.ssr.module-missing-server-view", "ssr", entry.line, `Module "${moduleDir}" renders ${entry.clientMass} JSX elements from client files only; it declares no Unit or View server component.`));
|
|
199
218
|
}
|
|
200
219
|
#getMountLoadWarnings(sourceFile) {
|
|
201
220
|
const warnings = [];
|
|
202
|
-
|
|
203
|
-
if (this.#isMountEffect(sourceFile.sourceFile, node))
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
severity: "warning",
|
|
209
|
-
file: sourceFile.file,
|
|
210
|
-
line: this.#getLine(sourceFile.sourceFile, load.node),
|
|
211
|
-
message: `Mount-only effect loads server data with ${load.callee}(). The route can fetch this before the first byte instead.`
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
ts2.forEachChild(node, visit);
|
|
216
|
-
};
|
|
217
|
-
ts2.forEachChild(sourceFile.sourceFile, visit);
|
|
221
|
+
SsrScanner.#walk(sourceFile.sourceFile, (node) => {
|
|
222
|
+
if (!this.#isMountEffect(sourceFile.sourceFile, node))
|
|
223
|
+
return;
|
|
224
|
+
for (const load of this.#getLoadCalls(sourceFile.sourceFile, node))
|
|
225
|
+
warnings.push(fileWarning("akan.ssr.client-mount-load", "ssr", sourceFile.file, `Mount-only effect loads server data with ${load.callee}(). The route can fetch this before the first byte instead.`, this.#getLine(sourceFile.sourceFile, load.node)));
|
|
226
|
+
});
|
|
218
227
|
return warnings;
|
|
219
228
|
}
|
|
220
229
|
#isMountEffect(sourceFile, node) {
|
|
@@ -228,35 +237,23 @@ class SsrScanner {
|
|
|
228
237
|
}
|
|
229
238
|
#getLoadCalls(sourceFile, node) {
|
|
230
239
|
const calls = [];
|
|
231
|
-
|
|
232
|
-
if (ts2.isCallExpression(child))
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
};
|
|
239
|
-
ts2.forEachChild(node, visit);
|
|
240
|
+
SsrScanner.#walk(node, (child) => {
|
|
241
|
+
if (!ts2.isCallExpression(child))
|
|
242
|
+
return;
|
|
243
|
+
const callee = child.expression.getText(sourceFile);
|
|
244
|
+
if (/^fetch\.[a-z]/.test(callee) || /^st\.do\.(init|get|view|load|list|count|insight)[A-Z]/.test(callee))
|
|
245
|
+
calls.push({ callee, node: child });
|
|
246
|
+
});
|
|
240
247
|
return calls;
|
|
241
248
|
}
|
|
242
249
|
#getTemplateStateWarnings(sourceFile) {
|
|
243
250
|
if (!sourceFile.file.endsWith(".Template.tsx"))
|
|
244
251
|
return [];
|
|
245
252
|
const warnings = [];
|
|
246
|
-
|
|
247
|
-
if (ts2.isCallExpression(node) && node.expression.getText(sourceFile.sourceFile) === "useState")
|
|
248
|
-
warnings.push(
|
|
249
|
-
|
|
250
|
-
scope: "ssr",
|
|
251
|
-
severity: "warning",
|
|
252
|
-
file: sourceFile.file,
|
|
253
|
-
line: this.#getLine(sourceFile.sourceFile, node),
|
|
254
|
-
message: "Template holds form state in useState. Templates are store-driven and carry no local state."
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
ts2.forEachChild(node, visit);
|
|
258
|
-
};
|
|
259
|
-
ts2.forEachChild(sourceFile.sourceFile, visit);
|
|
253
|
+
SsrScanner.#walk(sourceFile.sourceFile, (node) => {
|
|
254
|
+
if (ts2.isCallExpression(node) && node.expression.getText(sourceFile.sourceFile) === "useState")
|
|
255
|
+
warnings.push(fileWarning("akan.ssr.template-client-state", "ssr", sourceFile.file, "Template holds form state in useState. Templates are store-driven and carry no local state.", this.#getLine(sourceFile.sourceFile, node)));
|
|
256
|
+
});
|
|
260
257
|
return warnings;
|
|
261
258
|
}
|
|
262
259
|
#measureBalance(sourceFiles) {
|
|
@@ -311,7 +308,7 @@ class SsrScanner {
|
|
|
311
308
|
}
|
|
312
309
|
#getTouches(sourceFile, node) {
|
|
313
310
|
const touches = [];
|
|
314
|
-
|
|
311
|
+
SsrScanner.#walk(node, (child) => {
|
|
315
312
|
if (ts2.isCallExpression(child)) {
|
|
316
313
|
const callee = child.expression.getText(sourceFile);
|
|
317
314
|
const bareName = callee.split(".").pop() ?? callee;
|
|
@@ -329,31 +326,32 @@ class SsrScanner {
|
|
|
329
326
|
else if (SsrScanner.#clientGlobals.has(root))
|
|
330
327
|
touches.push(root);
|
|
331
328
|
}
|
|
332
|
-
|
|
333
|
-
};
|
|
334
|
-
ts2.forEachChild(node, visit);
|
|
329
|
+
});
|
|
335
330
|
return touches;
|
|
336
331
|
}
|
|
337
332
|
#getTagNames(sourceFile, node) {
|
|
338
333
|
const tags = new Set;
|
|
339
|
-
|
|
334
|
+
SsrScanner.#walk(node, (child) => {
|
|
340
335
|
if (ts2.isJsxOpeningElement(child) || ts2.isJsxSelfClosingElement(child))
|
|
341
336
|
tags.add(child.tagName.getText(sourceFile).split(".")[0]);
|
|
342
|
-
|
|
343
|
-
};
|
|
344
|
-
ts2.forEachChild(node, visit);
|
|
337
|
+
});
|
|
345
338
|
return tags;
|
|
346
339
|
}
|
|
347
340
|
#getMass(node) {
|
|
348
341
|
let mass = 0;
|
|
349
|
-
|
|
342
|
+
SsrScanner.#walk(node, (child) => {
|
|
350
343
|
if (ts2.isJsxOpeningElement(child) || ts2.isJsxSelfClosingElement(child))
|
|
351
344
|
mass += 1;
|
|
352
|
-
|
|
353
|
-
};
|
|
354
|
-
ts2.forEachChild(node, visit);
|
|
345
|
+
});
|
|
355
346
|
return mass;
|
|
356
347
|
}
|
|
348
|
+
static #walk(node, visit) {
|
|
349
|
+
const walk = (child) => {
|
|
350
|
+
visit(child);
|
|
351
|
+
ts2.forEachChild(child, walk);
|
|
352
|
+
};
|
|
353
|
+
ts2.forEachChild(node, walk);
|
|
354
|
+
}
|
|
357
355
|
#hasUseClient(sourceFile) {
|
|
358
356
|
const first = sourceFile.statements[0];
|
|
359
357
|
if (!first || !ts2.isExpressionStatement(first) || !ts2.isStringLiteral(first.expression))
|
|
@@ -491,7 +489,7 @@ var RULE_FIXES = {
|
|
|
491
489
|
"akan.global.duplicate-exported-function-body": "Extract the shared implementation into a single exported helper and import it, instead of copying the body.",
|
|
492
490
|
"akan.file.recommended-max-lines": "Split the file by responsibility \u2014 move Zones, Utils, or subcomponents into sibling files.",
|
|
493
491
|
"akan.file.max-lines": "Break the file into smaller focused modules; keep one primary responsibility per file.",
|
|
494
|
-
"akan.file.abstract-max-lines": "
|
|
492
|
+
"akan.file.abstract-max-lines": "Trim the abstract by hand to a title, one sentence naming what the module owns, a `## Rules` list of the invariants the source cannot show, and an optional workflow chain.",
|
|
495
493
|
"akan.file.placeholder-export": "Remove the placeholder export; generated indexes should only re-export real modules.",
|
|
496
494
|
"akan.file.bang-comment-in-client": "Rewrite the marker as `// FIXME:` or `// TODO:`. Keep the bang form for server, srvkit, and CLI files.",
|
|
497
495
|
"akan.file.dictionary-stale-text": "Replace the scaffold text with real localized copy for this dictionary entry.",
|
|
@@ -549,7 +547,7 @@ class AkanQualityScanner {
|
|
|
549
547
|
const ignoreFilter = ignore().add(await this.#readGitIgnore(workspaceRoot));
|
|
550
548
|
const files = [];
|
|
551
549
|
for (const targetRoot of ["apps", "libs"]) {
|
|
552
|
-
const absoluteTargetRoot =
|
|
550
|
+
const absoluteTargetRoot = path2.join(workspaceRoot, targetRoot);
|
|
553
551
|
if (!await isDirectory(absoluteTargetRoot))
|
|
554
552
|
continue;
|
|
555
553
|
await this.#walkTargetFiles(workspaceRoot, absoluteTargetRoot, ignoreFilter, files);
|
|
@@ -557,7 +555,7 @@ class AkanQualityScanner {
|
|
|
557
555
|
return files.sort();
|
|
558
556
|
}
|
|
559
557
|
async#readGitIgnore(workspaceRoot) {
|
|
560
|
-
const gitIgnorePath =
|
|
558
|
+
const gitIgnorePath = path2.join(workspaceRoot, ".gitignore");
|
|
561
559
|
if (!await Bun.file(gitIgnorePath).exists())
|
|
562
560
|
return [];
|
|
563
561
|
return (await readFile(gitIgnorePath, "utf8")).split(/\r?\n/);
|
|
@@ -565,8 +563,8 @@ class AkanQualityScanner {
|
|
|
565
563
|
async#walkTargetFiles(workspaceRoot, currentPath, ignoreFilter, files) {
|
|
566
564
|
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
567
565
|
for (const entry of entries) {
|
|
568
|
-
const absolutePath =
|
|
569
|
-
const relativePath = toPosix(
|
|
566
|
+
const absolutePath = path2.join(currentPath, entry.name);
|
|
567
|
+
const relativePath = toPosix(path2.relative(workspaceRoot, absolutePath));
|
|
570
568
|
if (shouldSkipPath(relativePath, entry.isDirectory(), ignoreFilter))
|
|
571
569
|
continue;
|
|
572
570
|
if (entry.isDirectory()) {
|
|
@@ -581,7 +579,7 @@ class AkanQualityScanner {
|
|
|
581
579
|
}
|
|
582
580
|
}
|
|
583
581
|
async#readSourceFile(workspaceRoot, file) {
|
|
584
|
-
const absolutePath =
|
|
582
|
+
const absolutePath = path2.join(workspaceRoot, file);
|
|
585
583
|
const content = await readFile(absolutePath, "utf8");
|
|
586
584
|
return {
|
|
587
585
|
file,
|
|
@@ -591,7 +589,7 @@ class AkanQualityScanner {
|
|
|
591
589
|
};
|
|
592
590
|
}
|
|
593
591
|
async#readTextFile(workspaceRoot, file) {
|
|
594
|
-
return { file, content: await readFile(
|
|
592
|
+
return { file, content: await readFile(path2.join(workspaceRoot, file), "utf8") };
|
|
595
593
|
}
|
|
596
594
|
#scanGlobalQuality(sourceFiles) {
|
|
597
595
|
const exportedFunctionLikes = sourceFiles.flatMap((sourceFile) => getExportedFunctionLikes(sourceFile));
|
|
@@ -627,24 +625,10 @@ class AkanQualityScanner {
|
|
|
627
625
|
const warnings = [];
|
|
628
626
|
const lineCount = sourceFile.content.split(/\r?\n/).length;
|
|
629
627
|
const recommendedLineLimit = getRecommendedLineLimit(sourceFile.file);
|
|
630
|
-
if (recommendedLineLimit && lineCount > recommendedLineLimit)
|
|
631
|
-
warnings.push({
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
severity: "warning",
|
|
635
|
-
file: sourceFile.file,
|
|
636
|
-
message: `File has ${lineCount} lines. Recommended limit for this file type is ${recommendedLineLimit} lines.`
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
if (lineCount > MAX_FILE_LINES) {
|
|
640
|
-
warnings.push({
|
|
641
|
-
rule: "akan.file.max-lines",
|
|
642
|
-
scope: "file",
|
|
643
|
-
severity: "warning",
|
|
644
|
-
file: sourceFile.file,
|
|
645
|
-
message: `File has ${lineCount} lines. Keep single files under ${MAX_FILE_LINES} lines.`
|
|
646
|
-
});
|
|
647
|
-
}
|
|
628
|
+
if (recommendedLineLimit && lineCount > recommendedLineLimit)
|
|
629
|
+
warnings.push(fileWarning("akan.file.recommended-max-lines", "file", sourceFile.file, `File has ${lineCount} lines. Recommended limit for this file type is ${recommendedLineLimit} lines.`));
|
|
630
|
+
if (lineCount > MAX_FILE_LINES)
|
|
631
|
+
warnings.push(fileWarning("akan.file.max-lines", "file", sourceFile.file, `File has ${lineCount} lines. Keep single files under ${MAX_FILE_LINES} lines.`));
|
|
648
632
|
warnings.push(...getBangCommentWarnings(sourceFile));
|
|
649
633
|
warnings.push(...getPlaceholderExportWarnings(sourceFile));
|
|
650
634
|
warnings.push(...getDictionaryTextWarnings(sourceFile));
|
|
@@ -658,14 +642,7 @@ class AkanQualityScanner {
|
|
|
658
642
|
continue;
|
|
659
643
|
if (declaration.kind === "interface" && allowedInterfaceNames.has(declaration.name))
|
|
660
644
|
continue;
|
|
661
|
-
warnings.push({
|
|
662
|
-
rule: "akan.file.class-export-global-declaration",
|
|
663
|
-
scope: "file",
|
|
664
|
-
severity: "warning",
|
|
665
|
-
file: sourceFile.file,
|
|
666
|
-
line: declaration.line,
|
|
667
|
-
message: `Class export files should not declare top-level ${declaration.kind} "${declaration.name}". Move helpers to another file and import them.`
|
|
668
|
-
});
|
|
645
|
+
warnings.push(fileWarning("akan.file.class-export-global-declaration", "file", sourceFile.file, `Class export files should not declare top-level ${declaration.kind} "${declaration.name}". Move helpers to another file and import them.`, declaration.line));
|
|
669
646
|
}
|
|
670
647
|
return warnings;
|
|
671
648
|
}
|
|
@@ -687,28 +664,13 @@ class AkanQualityScanner {
|
|
|
687
664
|
if (declaration.exported) {
|
|
688
665
|
if (isAllowedComponentExport(declaration, isPage))
|
|
689
666
|
continue;
|
|
690
|
-
warnings.push({
|
|
691
|
-
rule: "akan.file.component-export",
|
|
692
|
-
scope: "file",
|
|
693
|
-
severity: "warning",
|
|
694
|
-
file: sourceFile.file,
|
|
695
|
-
line: declaration.line,
|
|
696
|
-
message: `Component file exports ${declaration.kind} "${declaration.name}", which is not a PascalCase component${isPage ? " or reserved route export" : ""}.`
|
|
697
|
-
});
|
|
667
|
+
warnings.push(fileWarning("akan.file.component-export", "file", sourceFile.file, `Component file exports ${declaration.kind} "${declaration.name}", which is not a PascalCase component${isPage ? " or reserved route export" : ""}.`, declaration.line));
|
|
698
668
|
continue;
|
|
699
669
|
}
|
|
700
670
|
if (isComponentValueKind(declaration.kind) && compoundComponentNames.has(declaration.name))
|
|
701
671
|
continue;
|
|
702
|
-
if (isRestrictedInternalKind(declaration.kind))
|
|
703
|
-
warnings.push({
|
|
704
|
-
rule: "akan.file.component-internal-declaration",
|
|
705
|
-
scope: "file",
|
|
706
|
-
severity: "warning",
|
|
707
|
-
file: sourceFile.file,
|
|
708
|
-
line: declaration.line,
|
|
709
|
-
message: `Component file declares non-exported ${declaration.kind} "${declaration.name}". Only "interface <Component>Props" may stay internal.`
|
|
710
|
-
});
|
|
711
|
-
}
|
|
672
|
+
if (isRestrictedInternalKind(declaration.kind))
|
|
673
|
+
warnings.push(fileWarning("akan.file.component-internal-declaration", "file", sourceFile.file, `Component file declares non-exported ${declaration.kind} "${declaration.name}". Only "interface <Component>Props" may stay internal.`, declaration.line));
|
|
712
674
|
}
|
|
713
675
|
return warnings;
|
|
714
676
|
}
|
|
@@ -716,34 +678,15 @@ class AkanQualityScanner {
|
|
|
716
678
|
const suffix = CONVENTION_SUFFIXES.find((candidate) => sourceFile.file.endsWith(candidate));
|
|
717
679
|
if (!suffix)
|
|
718
680
|
return [];
|
|
719
|
-
const modelName = toPascalCase(
|
|
720
|
-
|
|
721
|
-
for (const declaration of getTopLevelDeclarations(sourceFile)) {
|
|
722
|
-
if (isAllowedConventionDeclaration(suffix, modelName, declaration))
|
|
723
|
-
continue;
|
|
724
|
-
warnings.push({
|
|
725
|
-
rule: `akan.convention${suffix.replace(".ts", "")}`,
|
|
726
|
-
scope: "convention",
|
|
727
|
-
severity: "warning",
|
|
728
|
-
file: sourceFile.file,
|
|
729
|
-
line: declaration.line,
|
|
730
|
-
message: `${path.basename(sourceFile.file)} should not declare top-level ${declaration.kind} "${declaration.name}". Allowed declarations: ${getConventionDescription(suffix, modelName)}.`
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
return warnings;
|
|
681
|
+
const modelName = toPascalCase(path2.basename(sourceFile.file, suffix));
|
|
682
|
+
return getTopLevelDeclarations(sourceFile).filter((declaration) => !isAllowedConventionDeclaration(suffix, modelName, declaration)).map((declaration) => fileWarning(`akan.convention${suffix.replace(".ts", "")}`, "convention", sourceFile.file, `${path2.basename(sourceFile.file)} should not declare top-level ${declaration.kind} "${declaration.name}". Allowed declarations: ${getConventionDescription(suffix, modelName)}.`, declaration.line));
|
|
734
683
|
}
|
|
735
684
|
#scanAbstractQuality({ file, content }) {
|
|
736
685
|
const lineCount = AbstractDoc.lineCountOf(content);
|
|
737
686
|
if (lineCount <= AbstractDoc.maxLines)
|
|
738
687
|
return [];
|
|
739
688
|
return [
|
|
740
|
-
{
|
|
741
|
-
rule: "akan.file.abstract-max-lines",
|
|
742
|
-
scope: "file",
|
|
743
|
-
severity: "warning",
|
|
744
|
-
file,
|
|
745
|
-
message: `Abstract has ${lineCount} lines. Keep abstracts under ${AbstractDoc.maxLines} lines and compact them periodically.`
|
|
746
|
-
}
|
|
689
|
+
fileWarning("akan.file.abstract-max-lines", "file", file, `Abstract has ${lineCount} lines. Keep abstracts under ${AbstractDoc.maxLines} lines and compact them periodically.`)
|
|
747
690
|
];
|
|
748
691
|
}
|
|
749
692
|
#scanLayoutQuality(sourceFile) {
|
|
@@ -753,26 +696,12 @@ class AkanQualityScanner {
|
|
|
753
696
|
const { type, name, isDir } = rootEntry;
|
|
754
697
|
const allowed = isDir ? rootAllowedDirs[type].has(name) : rootAllowedFiles[type].has(name);
|
|
755
698
|
const kind = isDir ? "folder" : "file";
|
|
756
|
-
if (!allowed)
|
|
757
|
-
warnings.push({
|
|
758
|
-
rule: `akan.layout.${type}-root-${kind}`,
|
|
759
|
-
scope: "layout",
|
|
760
|
-
severity: "warning",
|
|
761
|
-
file: sourceFile.file,
|
|
762
|
-
message: `Unexpected ${type} root ${kind} "${name}". Keep ${type} code in conventional ${type} folders.`
|
|
763
|
-
});
|
|
764
|
-
}
|
|
699
|
+
if (!allowed)
|
|
700
|
+
warnings.push(fileWarning(`akan.layout.${type}-root-${kind}`, "layout", sourceFile.file, `Unexpected ${type} root ${kind} "${name}". ${rootEntryHintOf(type, name) ?? `Keep ${type} code in conventional ${type} folders.`}`));
|
|
765
701
|
}
|
|
766
702
|
const libFacetFile = getLibFacetRootFile(sourceFile.file);
|
|
767
|
-
if (libFacetFile && !isAllowedLibFacetRootFile(libFacetFile))
|
|
768
|
-
warnings.push({
|
|
769
|
-
rule: "akan.layout.lib-facet-file",
|
|
770
|
-
scope: "layout",
|
|
771
|
-
severity: "warning",
|
|
772
|
-
file: sourceFile.file,
|
|
773
|
-
message: `Unexpected lib facet root file "${libFacetFile}". Keep direct lib/ root files limited to generated support facets.`
|
|
774
|
-
});
|
|
775
|
-
}
|
|
703
|
+
if (libFacetFile && !isAllowedLibFacetRootFile(libFacetFile))
|
|
704
|
+
warnings.push(fileWarning("akan.layout.lib-facet-file", "layout", sourceFile.file, `Unexpected lib facet root file "${libFacetFile}". Keep direct lib/ root files limited to generated support facets.`));
|
|
776
705
|
const moduleUiWarning = getModuleUiWarning(sourceFile.file);
|
|
777
706
|
if (moduleUiWarning)
|
|
778
707
|
warnings.push(moduleUiWarning);
|
|
@@ -839,67 +768,48 @@ function formatQualityLocation(file, line) {
|
|
|
839
768
|
function getExportedFunctionLikes(sourceFile) {
|
|
840
769
|
const declarations = [];
|
|
841
770
|
const nameExempt = isPageRouteFile(sourceFile.file) || isUiComponentFile(sourceFile.file);
|
|
771
|
+
const add = (name, kind, node, body, isEnumClass = false) => declarations.push({
|
|
772
|
+
name,
|
|
773
|
+
kind,
|
|
774
|
+
file: sourceFile.file,
|
|
775
|
+
line: getLine(sourceFile.sourceFile, node),
|
|
776
|
+
bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, body),
|
|
777
|
+
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClass)
|
|
778
|
+
});
|
|
842
779
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
843
|
-
if (ts3.isFunctionDeclaration(statement) && statement.name && isExported(statement))
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
file: sourceFile.file,
|
|
848
|
-
line: getLine(sourceFile.sourceFile, statement),
|
|
849
|
-
bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, statement.body),
|
|
850
|
-
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false)
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
if (ts3.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
854
|
-
declarations.push({
|
|
855
|
-
name: statement.name.text,
|
|
856
|
-
kind: "class",
|
|
857
|
-
file: sourceFile.file,
|
|
858
|
-
line: getLine(sourceFile.sourceFile, statement),
|
|
859
|
-
bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, statement),
|
|
860
|
-
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClassStatement(sourceFile.sourceFile, statement))
|
|
861
|
-
});
|
|
862
|
-
}
|
|
780
|
+
if (ts3.isFunctionDeclaration(statement) && statement.name && isExported(statement))
|
|
781
|
+
add(statement.name.text, "function", statement, statement.body);
|
|
782
|
+
if (ts3.isClassDeclaration(statement) && statement.name && isExported(statement))
|
|
783
|
+
add(statement.name.text, "class", statement, statement, isEnumClassStatement(sourceFile.sourceFile, statement));
|
|
863
784
|
if (ts3.isVariableStatement(statement) && isExported(statement)) {
|
|
864
785
|
for (const declaration of statement.declarationList.declarations) {
|
|
865
786
|
if (!ts3.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
|
|
866
787
|
continue;
|
|
867
|
-
|
|
868
|
-
name: declaration.name.text,
|
|
869
|
-
kind: "function-variable",
|
|
870
|
-
file: sourceFile.file,
|
|
871
|
-
line: getLine(sourceFile.sourceFile, declaration),
|
|
872
|
-
bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, declaration.initializer),
|
|
873
|
-
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false)
|
|
874
|
-
});
|
|
788
|
+
add(declaration.name.text, "function-variable", declaration, declaration.initializer);
|
|
875
789
|
}
|
|
876
790
|
}
|
|
877
791
|
}
|
|
878
792
|
return declarations;
|
|
879
793
|
}
|
|
880
|
-
function
|
|
794
|
+
function sysSegments(file) {
|
|
881
795
|
const segments = file.split("/");
|
|
882
|
-
return
|
|
796
|
+
return segments[0] === "apps" || segments[0] === "libs" ? segments : null;
|
|
797
|
+
}
|
|
798
|
+
function isPageRouteFile(file) {
|
|
799
|
+
return sysSegments(file)?.[2] === "page";
|
|
883
800
|
}
|
|
884
801
|
function isUiComponentFile(file) {
|
|
885
|
-
|
|
886
|
-
return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "ui";
|
|
802
|
+
return sysSegments(file)?.[2] === "ui";
|
|
887
803
|
}
|
|
888
804
|
function isConventionDuplicateNameExempt(file, isEnumClass) {
|
|
889
|
-
if (!
|
|
805
|
+
if (!sysSegments(file)?.includes("lib"))
|
|
890
806
|
return false;
|
|
891
|
-
if (file.endsWith(".tsx"))
|
|
892
|
-
return true;
|
|
893
|
-
if (file.endsWith(".document.ts") || file.endsWith(".service.ts") || file.endsWith(".signal.ts") || file.endsWith(".store.ts"))
|
|
807
|
+
if (file.endsWith(".tsx") || /\.(document|service|signal|store)\.ts$/.test(file))
|
|
894
808
|
return true;
|
|
895
809
|
if (file.endsWith(".constant.ts"))
|
|
896
810
|
return !isEnumClass;
|
|
897
811
|
return false;
|
|
898
812
|
}
|
|
899
|
-
function isInLibModule(file) {
|
|
900
|
-
const segments = file.split("/");
|
|
901
|
-
return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
|
|
902
|
-
}
|
|
903
813
|
function isEnumClassStatement(sourceFile, statement) {
|
|
904
814
|
if (!ts3.isClassDeclaration(statement))
|
|
905
815
|
return false;
|
|
@@ -995,14 +905,7 @@ function isDefaultExportStatement(statement) {
|
|
|
995
905
|
function getBangCommentWarnings(sourceFile) {
|
|
996
906
|
if (!isClientReachableFile(sourceFile.file))
|
|
997
907
|
return [];
|
|
998
|
-
return findPatternLines(sourceFile.content, /(?:^|[^\s/][ \t]+)\/[*/]!/).map((line) => (
|
|
999
|
-
rule: "akan.file.bang-comment-in-client",
|
|
1000
|
-
scope: "file",
|
|
1001
|
-
severity: "warning",
|
|
1002
|
-
file: sourceFile.file,
|
|
1003
|
-
line,
|
|
1004
|
-
message: "A `//!` or `/*!` marker survives minification and ships to the browser."
|
|
1005
|
-
}));
|
|
908
|
+
return findPatternLines(sourceFile.content, /(?:^|[^\s/][ \t]+)\/[*/]!/).map((line) => fileWarning("akan.file.bang-comment-in-client", "file", sourceFile.file, "A `//!` or `/*!` marker survives minification and ships to the browser.", line));
|
|
1006
909
|
}
|
|
1007
910
|
function isClientReachableFile(file) {
|
|
1008
911
|
if (/\.(test|spec)\.tsx?$/.test(file))
|
|
@@ -1021,61 +924,24 @@ function isClientReachableFile(file) {
|
|
|
1021
924
|
function getPlaceholderExportWarnings(sourceFile) {
|
|
1022
925
|
if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx"))
|
|
1023
926
|
return [];
|
|
1024
|
-
return getTopLevelDeclarations(sourceFile).filter((declaration) => declaration.exported && PLACEHOLDER_EXPORT_NAMES.has(declaration.name)).map((declaration) => ({
|
|
1025
|
-
rule: "akan.file.placeholder-export",
|
|
1026
|
-
scope: "file",
|
|
1027
|
-
severity: "warning",
|
|
1028
|
-
file: sourceFile.file,
|
|
1029
|
-
line: declaration.line,
|
|
1030
|
-
message: `Generated or barrel index file should not export placeholder "${declaration.name}".`
|
|
1031
|
-
}));
|
|
927
|
+
return getTopLevelDeclarations(sourceFile).filter((declaration) => declaration.exported && PLACEHOLDER_EXPORT_NAMES.has(declaration.name)).map((declaration) => fileWarning("akan.file.placeholder-export", "file", sourceFile.file, `Generated or barrel index file should not export placeholder "${declaration.name}".`, declaration.line));
|
|
1032
928
|
}
|
|
1033
929
|
function getDictionaryTextWarnings(sourceFile) {
|
|
1034
930
|
if (!sourceFile.file.endsWith(".dictionary.ts"))
|
|
1035
931
|
return [];
|
|
1036
932
|
const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
|
|
1037
|
-
return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile.content, pattern).map((line) => ({
|
|
1038
|
-
rule: "akan.file.dictionary-stale-text",
|
|
1039
|
-
scope: "file",
|
|
1040
|
-
severity: "warning",
|
|
1041
|
-
file: sourceFile.file,
|
|
1042
|
-
line,
|
|
1043
|
-
message: `Dictionary text appears to contain ${label}.`
|
|
1044
|
-
})));
|
|
933
|
+
return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile.content, pattern).map((line) => fileWarning("akan.file.dictionary-stale-text", "file", sourceFile.file, `Dictionary text appears to contain ${label}.`, line)));
|
|
1045
934
|
}
|
|
1046
935
|
function getGlobalMutationWarnings(sourceFile) {
|
|
1047
936
|
const warnings = [];
|
|
1048
937
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
message: "Global declarations require an explicit low-level integration allowlist."
|
|
1057
|
-
});
|
|
1058
|
-
}
|
|
1059
|
-
if (ts3.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
|
|
1060
|
-
warnings.push({
|
|
1061
|
-
rule: "akan.file.window-augmentation",
|
|
1062
|
-
scope: "file",
|
|
1063
|
-
severity: "warning",
|
|
1064
|
-
file: sourceFile.file,
|
|
1065
|
-
line: getLine(sourceFile.sourceFile, statement),
|
|
1066
|
-
message: "Window augmentation should be isolated to approved browser integration files."
|
|
1067
|
-
});
|
|
1068
|
-
}
|
|
1069
|
-
if (ts3.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype.")) {
|
|
1070
|
-
warnings.push({
|
|
1071
|
-
rule: "akan.file.prototype-mutation",
|
|
1072
|
-
scope: "file",
|
|
1073
|
-
severity: "warning",
|
|
1074
|
-
file: sourceFile.file,
|
|
1075
|
-
line: getLine(sourceFile.sourceFile, statement),
|
|
1076
|
-
message: "Prototype mutation should be avoided or isolated to approved low-level integration files."
|
|
1077
|
-
});
|
|
1078
|
-
}
|
|
938
|
+
const warn = (rule, message) => warnings.push(fileWarning(rule, "file", sourceFile.file, message, getLine(sourceFile.sourceFile, statement)));
|
|
939
|
+
if (ts3.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global")
|
|
940
|
+
warn("akan.file.global-declaration", "Global declarations require an explicit low-level integration allowlist.");
|
|
941
|
+
if (ts3.isInterfaceDeclaration(statement) && statement.name.text === "Window")
|
|
942
|
+
warn("akan.file.window-augmentation", "Window augmentation should be isolated to approved browser integration files.");
|
|
943
|
+
if (ts3.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype."))
|
|
944
|
+
warn("akan.file.prototype-mutation", "Prototype mutation should be avoided or isolated to approved low-level integration files.");
|
|
1079
945
|
}
|
|
1080
946
|
return warnings;
|
|
1081
947
|
}
|
|
@@ -1084,73 +950,59 @@ function getTopLevelDeclarations(sourceFile) {
|
|
|
1084
950
|
}
|
|
1085
951
|
function getTopLevelDeclaration(sourceFile, statement) {
|
|
1086
952
|
const line = getLine(sourceFile, statement);
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
if (ts3.
|
|
1097
|
-
return [
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
if (ts3.
|
|
1103
|
-
return statement.
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
exported: isExported(statement),
|
|
1108
|
-
node: statement
|
|
1109
|
-
}));
|
|
1110
|
-
}
|
|
1111
|
-
if (ts3.isExportDeclaration(statement)) {
|
|
953
|
+
const declared = (name, kind, at = line) => ({
|
|
954
|
+
name,
|
|
955
|
+
kind,
|
|
956
|
+
line: at,
|
|
957
|
+
exported: isExported(statement),
|
|
958
|
+
node: statement
|
|
959
|
+
});
|
|
960
|
+
if (ts3.isClassDeclaration(statement) && statement.name)
|
|
961
|
+
return [declared(statement.name.text, "class")];
|
|
962
|
+
if (ts3.isFunctionDeclaration(statement) && statement.name)
|
|
963
|
+
return [declared(statement.name.text, "function")];
|
|
964
|
+
if (ts3.isInterfaceDeclaration(statement))
|
|
965
|
+
return [declared(statement.name.text, "interface")];
|
|
966
|
+
if (ts3.isTypeAliasDeclaration(statement))
|
|
967
|
+
return [declared(statement.name.text, "type")];
|
|
968
|
+
if (ts3.isEnumDeclaration(statement))
|
|
969
|
+
return [declared(statement.name.text, "enum")];
|
|
970
|
+
if (ts3.isVariableStatement(statement))
|
|
971
|
+
return statement.declarationList.declarations.filter((declaration) => ts3.isIdentifier(declaration.name)).map((declaration) => declared(declaration.name.text, "variable", getLine(sourceFile, declaration)));
|
|
972
|
+
if (ts3.isExportDeclaration(statement))
|
|
1112
973
|
return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
|
|
1113
|
-
}
|
|
1114
974
|
return [];
|
|
1115
975
|
}
|
|
1116
|
-
function
|
|
1117
|
-
if (suffix === ".dictionary.ts")
|
|
1118
|
-
return isExportedConst(declaration) && declaration.name === "dictionary";
|
|
976
|
+
function getConventionClassNames(suffix, model) {
|
|
1119
977
|
if (suffix === ".constant.ts")
|
|
1120
|
-
return
|
|
978
|
+
return [`${model}Input`, `${model}Object`, model, `Light${model}`, `${model}Insight`];
|
|
1121
979
|
if (suffix === ".document.ts")
|
|
1122
|
-
return
|
|
980
|
+
return [`${model}Filter`, model, `${model}Model`];
|
|
1123
981
|
if (suffix === ".service.ts")
|
|
1124
|
-
return
|
|
982
|
+
return [`${model}Service`];
|
|
1125
983
|
if (suffix === ".signal.ts")
|
|
1126
|
-
return
|
|
984
|
+
return [`${model}Internal`, `${model}Slice`, `${model}Endpoint`];
|
|
1127
985
|
if (suffix === ".store.ts")
|
|
1128
|
-
return
|
|
1129
|
-
return
|
|
986
|
+
return [`${model}Store`];
|
|
987
|
+
return [];
|
|
1130
988
|
}
|
|
1131
|
-
function
|
|
989
|
+
function isAllowedConventionDeclaration(suffix, modelName, declaration) {
|
|
990
|
+
if (suffix === ".dictionary.ts")
|
|
991
|
+
return isExportedConst(declaration) && declaration.name === "dictionary";
|
|
1132
992
|
if (declaration.kind !== "class")
|
|
1133
993
|
return false;
|
|
1134
|
-
if (
|
|
994
|
+
if (getConventionClassNames(suffix, modelName).includes(declaration.name))
|
|
1135
995
|
return true;
|
|
1136
|
-
|
|
1137
|
-
return false;
|
|
1138
|
-
const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token === ts3.SyntaxKind.ExtendsKeyword);
|
|
1139
|
-
const expression = heritageClause?.types[0]?.expression;
|
|
1140
|
-
return !!expression && expression.getText().startsWith("enumOf(");
|
|
996
|
+
return suffix === ".constant.ts" && isEnumClassStatement(declaration.node.getSourceFile(), declaration.node);
|
|
1141
997
|
}
|
|
1142
998
|
function getConventionDescription(suffix, modelName) {
|
|
1143
999
|
if (suffix === ".dictionary.ts")
|
|
1144
1000
|
return "export const dictionary";
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
return `${modelName}Service`;
|
|
1151
|
-
if (suffix === ".signal.ts")
|
|
1152
|
-
return `${modelName}Internal, ${modelName}Slice, or ${modelName}Endpoint`;
|
|
1153
|
-
return `${modelName}Store`;
|
|
1001
|
+
const names = [
|
|
1002
|
+
...getConventionClassNames(suffix, modelName),
|
|
1003
|
+
...suffix === ".constant.ts" ? ["enumOf classes"] : []
|
|
1004
|
+
];
|
|
1005
|
+
return names.length > 1 ? `${names.slice(0, -1).join(", ")}, or ${names.at(-1)}` : names[0];
|
|
1154
1006
|
}
|
|
1155
1007
|
var sysRootTypes = { apps: "app", libs: "lib" };
|
|
1156
1008
|
function getSysRootEntry(file) {
|
|
@@ -1182,13 +1034,7 @@ function getModuleUiWarning(file) {
|
|
|
1182
1034
|
const allowedFileNames = new Set(allowedSuffixes.map((suffix) => `${pascalName}.${suffix}.tsx`));
|
|
1183
1035
|
if (allowedFileNames.has(fileName) || fileName.endsWith(".test.tsx") || fileName.endsWith(".spec.tsx"))
|
|
1184
1036
|
return null;
|
|
1185
|
-
return {
|
|
1186
|
-
rule: "akan.layout.module-ui-file",
|
|
1187
|
-
scope: "layout",
|
|
1188
|
-
severity: "warning",
|
|
1189
|
-
file,
|
|
1190
|
-
message: `Unexpected ${kind} module UI filename "${fileName}". Expected one of: ${[...allowedFileNames].join(", ")}.`
|
|
1191
|
-
};
|
|
1037
|
+
return fileWarning("akan.layout.module-ui-file", "layout", file, `Unexpected ${kind} module UI filename "${fileName}". Expected one of: ${[...allowedFileNames].join(", ")}.`);
|
|
1192
1038
|
}
|
|
1193
1039
|
function getModuleInfo(file) {
|
|
1194
1040
|
const segments = file.split("/");
|
|
@@ -1204,10 +1050,8 @@ function getModuleInfo(file) {
|
|
|
1204
1050
|
if (segments.length !== libIndex + 3)
|
|
1205
1051
|
return null;
|
|
1206
1052
|
const fileName = segments[libIndex + 2];
|
|
1207
|
-
if (moduleName.startsWith("__"))
|
|
1208
|
-
|
|
1209
|
-
return { moduleName: scalarName, fileName, kind: "scalar" };
|
|
1210
|
-
}
|
|
1053
|
+
if (moduleName.startsWith("__"))
|
|
1054
|
+
return { moduleName, fileName, kind: "scalar" };
|
|
1211
1055
|
if (moduleName.startsWith("_"))
|
|
1212
1056
|
return { moduleName, fileName, kind: "service" };
|
|
1213
1057
|
return { moduleName, fileName, kind: "database" };
|
|
@@ -1216,7 +1060,7 @@ function isExportedConst(declaration) {
|
|
|
1216
1060
|
return declaration.exported && ts3.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts3.NodeFlags.Const) !== 0;
|
|
1217
1061
|
}
|
|
1218
1062
|
function isExported(node) {
|
|
1219
|
-
return !!
|
|
1063
|
+
return !!(ts3.getCombinedModifierFlags(node) & ts3.ModifierFlags.Export);
|
|
1220
1064
|
}
|
|
1221
1065
|
function isFunctionLikeInitializer(node) {
|
|
1222
1066
|
return !!node && (ts3.isArrowFunction(node) || ts3.isFunctionExpression(node));
|
|
@@ -1262,7 +1106,7 @@ function toPascalCase(value) {
|
|
|
1262
1106
|
return value.replace(/(^|[-_./])([a-zA-Z0-9])/g, (_, __, char) => char.toUpperCase()).replace(/[-_./]/g, "");
|
|
1263
1107
|
}
|
|
1264
1108
|
function toPosix(value) {
|
|
1265
|
-
return value.split(
|
|
1109
|
+
return value.split(path2.sep).join("/");
|
|
1266
1110
|
}
|
|
1267
1111
|
function groupBy(items, getKey) {
|
|
1268
1112
|
const grouped = new Map;
|
|
@@ -1279,24 +1123,16 @@ function compareWarnings(a, b) {
|
|
|
1279
1123
|
// pkgs/@akanjs/cli/quality/quality.script.ts
|
|
1280
1124
|
import { Logger } from "akanjs/common";
|
|
1281
1125
|
|
|
1282
|
-
|
|
1283
|
-
class QualityRunner extends runner("quality") {
|
|
1284
|
-
async scan(workspace) {
|
|
1285
|
-
return await new AkanQualityScanner().scan(workspace.workspaceRoot);
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
// pkgs/@akanjs/cli/quality/quality.script.ts
|
|
1290
|
-
class QualityScript extends script("quality", [QualityRunner]) {
|
|
1126
|
+
class QualityScript extends script("quality") {
|
|
1291
1127
|
async scan(workspace, format = "text") {
|
|
1292
1128
|
const spinner = workspace.spinning("Scanning Akan code quality...");
|
|
1293
|
-
const result = await
|
|
1129
|
+
const result = await new AkanQualityScanner().scan(workspace.workspaceRoot);
|
|
1294
1130
|
spinner.succeed(`Quality scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
|
|
1295
1131
|
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatQualityScanResult(result));
|
|
1296
1132
|
}
|
|
1297
1133
|
async ssr(workspace, format = "text") {
|
|
1298
1134
|
const spinner = workspace.spinning("Measuring Akan server/client render balance...");
|
|
1299
|
-
const scanned = await
|
|
1135
|
+
const scanned = await new AkanQualityScanner().scan(workspace.workspaceRoot);
|
|
1300
1136
|
const result = { ...scanned, warnings: scanned.warnings.filter((warning) => warning.scope === "ssr") };
|
|
1301
1137
|
spinner.succeed(`SSR scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
|
|
1302
1138
|
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSsrScanResult(result));
|
|
@@ -1305,15 +1141,7 @@ class QualityScript extends script("quality", [QualityRunner]) {
|
|
|
1305
1141
|
|
|
1306
1142
|
// pkgs/@akanjs/cli/quality/quality.command.ts
|
|
1307
1143
|
class QualityCommand extends command("quality", [QualityScript], ({ public: target }) => ({
|
|
1308
|
-
quality: target({ desc: "Scan apps and libs for Akan code quality warnings" }).arg("action", String, {
|
|
1309
|
-
desc: "quality action",
|
|
1310
|
-
default: "scan",
|
|
1311
|
-
enum: ["scan", "ssr"]
|
|
1312
|
-
}).option("format", String, {
|
|
1313
|
-
desc: "output format",
|
|
1314
|
-
default: "text",
|
|
1315
|
-
enum: ["text", "json"]
|
|
1316
|
-
}).with(Workspace).exec(async function(action, format, workspace) {
|
|
1144
|
+
quality: target({ desc: "Scan apps and libs for Akan code quality warnings" }).arg("action", String, { desc: "quality action", default: "scan", enum: ["scan", "ssr"] }).option("format", String, { desc: "output format", default: "text", enum: ["text", "json"] }).with(Workspace).exec(async function(action, format, workspace) {
|
|
1317
1145
|
if (action === "scan")
|
|
1318
1146
|
await this.qualityScript.scan(workspace, format);
|
|
1319
1147
|
else if (action === "ssr")
|