@akanjs/cli 3.0.0-beta.2 → 3.0.0-beta.20
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-awv258wb.js +9 -0
- package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
- package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
- package/CodeTui-1nqh35ez.js +1457 -0
- package/README.md +3 -0
- package/TunnelShare-scf4e4pd.js +9 -0
- package/{agent.command-d2gfxfck.js → agent.command-1s9ag7sb.js} +8 -9
- package/akanCode.js +102 -0
- package/{application.command-pj2m7584.js → application.command-49hwy6va.js} +51 -78
- package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-ybyp97pv.js} +63 -64
- package/{applicationReleasePackager-4yasxwv9.js → applicationReleasePackager-esnrphw8.js} +40 -60
- package/buildBatch.proc.js +19 -24
- package/{capacitorApp-36rbr858.js → capacitorApp-1synz4en.js} +3 -4
- package/cloud.command-d7rqtzt9.js +94 -0
- package/cloud.script-bkbpcr96.js +19 -0
- package/code.command-nn48xzej.js +154 -0
- package/commandManifest.json +1 -1
- package/consoleToStderr-h5bryj94.js +7 -0
- package/context.command-s3mcmvpa.js +48 -0
- package/context.runner-k99nb5pk.js +21 -0
- package/{dependencyScanner-shs82ynp.js → dependencyScanner-ye2e6pdz.js} +2 -2
- package/{devTui-40ggmd99.js → devTui-q7k7bakd.js} +122 -40
- package/fileSys-5ngz93ga.js +9 -0
- package/guideline.command-sct4qnt5.js +42 -0
- package/guidelines/agentRule/agentRule.instruction.md +206 -25
- package/guidelines/conventions/conventions.instruction.md +79 -24
- package/guidelines/fieldRule/fieldRule.instruction.md +1 -0
- package/guidelines/mcpRule/mcpRule.instruction.md +7 -5
- package/guidelines/queryRule/queryRule.instruction.md +17 -7
- package/guidelines/runtimeRule/runtimeRule.instruction.md +74 -0
- package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
- package/guidelines/transportRule/transportRule.instruction.md +51 -0
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +66 -3
- package/incrementalBuilder.proc.js +31 -48
- package/{index-h7yw2z9n.js → index-0bq14vr1.js} +8 -14
- package/index-13ffe9wp.js +69 -0
- package/index-22d57v4c.js +266 -0
- package/{index-fz5q3gd7.js → index-31yr441d.js} +10 -17
- package/index-3hx1z96c.js +87 -0
- package/index-3sq321ak.js +53 -0
- package/{index-kcxmv48c.js → index-3t7g9e4r.js} +45 -79
- package/{index-q0y2jq9j.js → index-5b6tavv3.js} +6 -3
- package/{index-47pj3aee.js → index-5e0n4hqx.js} +11 -12
- package/{index-259dsnz3.js → index-5pc1qrbv.js} +5 -8
- package/index-7md2g9vw.js +6 -0
- package/index-8mna0947.js +187 -0
- package/{index-zry5nz81.js → index-8sf7wmdr.js} +51 -30
- package/{index-2tv3fdx4.js → index-9d9bgcx4.js} +342 -765
- package/index-9dp031by.js +12 -0
- package/{index-ne2qy0nk.js → index-ahemdr5n.js} +12 -12
- package/{index-ws5e0j51.js → index-as7xmyz8.js} +40 -24
- package/index-axzjaqve.js +1118 -0
- package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
- package/index-c6eggbee.js +123 -0
- package/index-d2242ksy.js +99 -0
- package/{index-a64esmtb.js → index-ddxvdd1p.js} +169 -305
- package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
- package/{index-pnqbkf5p.js → index-dzf3r49a.js} +6 -2
- package/{index-w8kmx1b6.js → index-fgq75haa.js} +198 -290
- package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
- package/{index-hwkygn9s.js → index-khpfpcsd.js} +302 -33
- package/{index-wt7qrtd4.js → index-mxav6j3c.js} +93 -232
- package/{index-gyd220p2.js → index-myhc3c1j.js} +554 -577
- package/{index-600zhnve.js → index-nv5976x3.js} +56 -197
- package/index-p6pbz9n5.js +25 -0
- package/{index-4edzf1gr.js → index-przybvy5.js} +77 -150
- package/index-smme4r47.js +49 -0
- package/{index-mak3cd28.js → index-w2zdcw3k.js} +137 -255
- package/index-wa5jbxj1.js +760 -0
- package/{index-v0qh106m.js → index-ww4z397j.js} +527 -723
- package/{index-j9yhg404.js → index-xqjwd384.js} +4 -7
- package/{index-7737gxcp.js → index-xqpzbab5.js} +107 -241
- package/index-xsjh6nzh.js +2729 -0
- package/{index-ktb5njgy.js → index-ygn77a5w.js} +47 -99
- package/index-ztp3e73t.js +700 -0
- package/index.js +23 -22
- package/{library.command-nbsky672.js → library.command-1gx04jgn.js} +4 -5
- package/{localRegistry.command-yeq57wjr.js → localRegistry.command-zndxj1q6.js} +22 -42
- package/{module.command-29vqzrh8.js → module.command-3waddk95.js} +8 -11
- package/{package.command-axv0abmn.js → package.command-zrc7br1y.js} +6 -7
- package/package.json +14 -5
- package/{page.command-vs9s3zrh.js → page.command-vpjgardh.js} +4 -5
- package/{primitive.command-cbxmqbtc.js → primitive.command-sz84tk4t.js} +9 -22
- package/prompter-ptaa3nk0.js +8 -0
- package/{quality.command-gmbs7nws.js → quality.command-g869hafs.js} +203 -376
- package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
- package/{repair.command-bqsjptsj.js → repair.command-chsnjvvv.js} +11 -35
- package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
- package/{scalar.command-6k1vhe9s.js → scalar.command-2v21newx.js} +7 -10
- 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-fwjfcts7.js → subspace.command-ckcbwfp1.js} +138 -67
- package/templates/__scalar/__model__/__model__.abstract.md.template +8 -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.abstract.ts +12 -32
- package/templates/appSample/lib/task/task.signal.spec.ts +15 -18
- package/templates/appSample/lib/task/task.signal.test.ts +15 -1
- package/templates/appSample/lib/task/task.signal.ts +1 -1
- package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +7 -4
- package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +6 -4
- 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 +0 -2
- package/templates/service/__model__.abstract.md.template +8 -0
- package/tunnel.command-kd2yr4fx.js +129 -0
- package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
- package/typecheck.proc.js +1 -1
- package/workflow.command-44252cqv.js +79 -0
- package/{workspace.command-8s050m0h.js → workspace.command-fqcztt5p.js} +77 -109
- package/abstractCompactor-mjh5ppsq.js +0 -101
- package/cloud.command-jshwdq22.js +0 -93
- package/context.command-ez8a0zyz.js +0 -80
- package/devTuiView-c08ccbat.js +0 -9
- package/getDirname-dr3jz1xg.js +0 -7
- package/guideline.command-xmqwx8zm.js +0 -359
- 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-2hq52jq3.js +0 -465
- package/index-6wwzs1bc.js +0 -7
- package/index-8pm30m73.js +0 -841
- package/index-kn1h7p1x.js +0 -560
- package/index-meh558jy.js +0 -65
- package/index-rwg2aa4f.js +0 -229
- 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/workflow.command-nwpcdc8p.js +0 -107
|
@@ -1,29 +1,88 @@
|
|
|
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-w2zdcw3k.js";
|
|
7
|
+
import"./index-5b6tavv3.js";
|
|
9
8
|
import {
|
|
10
9
|
rootAllowedFiles,
|
|
11
10
|
rootAllowedDirs,
|
|
12
11
|
isAllowedLibFacetRootFile
|
|
13
|
-
} from "./index-
|
|
14
|
-
import"./index-6wwzs1bc.js";
|
|
15
|
-
import {
|
|
16
|
-
AbstractDoc
|
|
17
|
-
} from "./index-meh558jy.js";
|
|
12
|
+
} from "./index-9d9bgcx4.js";
|
|
18
13
|
|
|
19
14
|
// pkgs/@akanjs/devkit/qualityScanner.ts
|
|
20
15
|
import { createHash } from "crypto";
|
|
21
16
|
import { readdir, readFile, stat } from "fs/promises";
|
|
22
|
-
import
|
|
17
|
+
import path2 from "path";
|
|
23
18
|
import { RESERVED_ROUTE_CONFIG_EXPORTS } from "akanjs/common";
|
|
24
19
|
import ignore from "ignore";
|
|
25
20
|
import ts3 from "typescript";
|
|
26
21
|
|
|
22
|
+
// pkgs/@akanjs/devkit/abstractDoc.ts
|
|
23
|
+
import path from "path";
|
|
24
|
+
|
|
25
|
+
class AbstractDoc {
|
|
26
|
+
path;
|
|
27
|
+
content;
|
|
28
|
+
siblingFiles;
|
|
29
|
+
static suffix = ".abstract.md";
|
|
30
|
+
static maxLines = 300;
|
|
31
|
+
static #facetRoots = "{lib,ui,webkit,srvkit,common,plugin}";
|
|
32
|
+
static isAbstractPath(filePath) {
|
|
33
|
+
return filePath.endsWith(AbstractDoc.suffix);
|
|
34
|
+
}
|
|
35
|
+
static lineCountOf(content) {
|
|
36
|
+
return content.split(/\r?\n/).length;
|
|
37
|
+
}
|
|
38
|
+
static kindOf(filePath) {
|
|
39
|
+
const [root, folder, ...rest] = filePath.split("/");
|
|
40
|
+
if (root !== "lib" || !folder)
|
|
41
|
+
return "other";
|
|
42
|
+
if (folder === "__scalar")
|
|
43
|
+
return rest.length === 2 ? "scalar" : "other";
|
|
44
|
+
if (rest.length !== 1)
|
|
45
|
+
return "other";
|
|
46
|
+
return folder.startsWith("_") ? "service" : "domain";
|
|
47
|
+
}
|
|
48
|
+
static async findAll(sys, { module } = {}) {
|
|
49
|
+
const filePaths = (await sys.getAllFiles(`${AbstractDoc.#facetRoots}/**/*${AbstractDoc.suffix}`)).map((filePath) => filePath.split(path.sep).join("/")).sort();
|
|
50
|
+
const docs = await Promise.all(filePaths.map((filePath) => AbstractDoc.read(sys, filePath)));
|
|
51
|
+
if (!module)
|
|
52
|
+
return docs;
|
|
53
|
+
return docs.filter((doc) => doc.moduleName === module || doc.folderName === module);
|
|
54
|
+
}
|
|
55
|
+
static async read(sys, filePath) {
|
|
56
|
+
const dirPath = filePath.split("/").slice(0, -1).join("/");
|
|
57
|
+
const [content, folderFiles] = await Promise.all([sys.readFile(filePath), sys.readdir(dirPath)]);
|
|
58
|
+
return new AbstractDoc(filePath, content, folderFiles.filter((fileName) => !AbstractDoc.isAbstractPath(fileName)));
|
|
59
|
+
}
|
|
60
|
+
constructor(path2, content, siblingFiles) {
|
|
61
|
+
this.path = path2;
|
|
62
|
+
this.content = content;
|
|
63
|
+
this.siblingFiles = siblingFiles;
|
|
64
|
+
}
|
|
65
|
+
get kind() {
|
|
66
|
+
return AbstractDoc.kindOf(this.path);
|
|
67
|
+
}
|
|
68
|
+
get folderName() {
|
|
69
|
+
return this.path.split("/").at(-2) ?? "";
|
|
70
|
+
}
|
|
71
|
+
get moduleName() {
|
|
72
|
+
return (this.path.split("/").at(-1) ?? "").slice(0, -AbstractDoc.suffix.length);
|
|
73
|
+
}
|
|
74
|
+
get lineCount() {
|
|
75
|
+
return AbstractDoc.lineCountOf(this.content);
|
|
76
|
+
}
|
|
77
|
+
canReplaceWith(candidate) {
|
|
78
|
+
const next = candidate.trim();
|
|
79
|
+
if (!next.startsWith("#"))
|
|
80
|
+
return false;
|
|
81
|
+
const lineCount = AbstractDoc.lineCountOf(next);
|
|
82
|
+
return lineCount >= 3 && lineCount < this.lineCount;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
27
86
|
// pkgs/@akanjs/devkit/formSetterScanner.ts
|
|
28
87
|
import ts from "typescript";
|
|
29
88
|
|
|
@@ -99,25 +158,11 @@ var STATIC_COMPONENT_MIN_MASS = 4;
|
|
|
99
158
|
var MIXED_COMPONENT_MIN_MASS = 10;
|
|
100
159
|
var MIXED_COMPONENT_MAX_TOUCHES = 2;
|
|
101
160
|
var MODULE_SERVER_VIEW_MIN_CLIENT_MASS = 12;
|
|
161
|
+
var fileWarning = (rule, scope, file, message, line) => line === undefined ? { rule, scope, severity: "warning", file, message } : { rule, scope, severity: "warning", file, line, message };
|
|
102
162
|
|
|
103
163
|
class SsrScanner {
|
|
104
164
|
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
|
-
]);
|
|
165
|
+
static #clientGlobals = new Set("window document navigator localStorage sessionStorage location history screen matchMedia IntersectionObserver".split(" ").concat("ResizeObserver MutationObserver requestAnimationFrame WebSocket".split(" ")));
|
|
121
166
|
static #clientRuntimeImports = new Set(["st", "fetch"]);
|
|
122
167
|
scan(sourceFiles) {
|
|
123
168
|
const componentFiles = sourceFiles.filter((sourceFile) => this.#isBalancedFile(sourceFile.file));
|
|
@@ -138,38 +183,17 @@ class SsrScanner {
|
|
|
138
183
|
const components = this.#getComponents(sourceFile, vendorNames);
|
|
139
184
|
const warnings = [];
|
|
140
185
|
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
|
-
});
|
|
186
|
+
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
187
|
}
|
|
150
188
|
for (const component of components) {
|
|
151
189
|
if (component.vendorTags)
|
|
152
190
|
continue;
|
|
153
191
|
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
|
-
});
|
|
192
|
+
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
193
|
continue;
|
|
163
194
|
}
|
|
164
195
|
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
|
-
});
|
|
196
|
+
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
197
|
}
|
|
174
198
|
}
|
|
175
199
|
warnings.push(...this.#getMountLoadWarnings(sourceFile));
|
|
@@ -189,32 +213,16 @@ class SsrScanner {
|
|
|
189
213
|
entry.serverFiles += 1;
|
|
190
214
|
modules.set(moduleDir, entry);
|
|
191
215
|
}
|
|
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
|
-
}));
|
|
216
|
+
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
217
|
}
|
|
200
218
|
#getMountLoadWarnings(sourceFile) {
|
|
201
219
|
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);
|
|
220
|
+
SsrScanner.#walk(sourceFile.sourceFile, (node) => {
|
|
221
|
+
if (!this.#isMountEffect(sourceFile.sourceFile, node))
|
|
222
|
+
return;
|
|
223
|
+
for (const load of this.#getLoadCalls(sourceFile.sourceFile, node))
|
|
224
|
+
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)));
|
|
225
|
+
});
|
|
218
226
|
return warnings;
|
|
219
227
|
}
|
|
220
228
|
#isMountEffect(sourceFile, node) {
|
|
@@ -228,35 +236,23 @@ class SsrScanner {
|
|
|
228
236
|
}
|
|
229
237
|
#getLoadCalls(sourceFile, node) {
|
|
230
238
|
const calls = [];
|
|
231
|
-
|
|
232
|
-
if (ts2.isCallExpression(child))
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
};
|
|
239
|
-
ts2.forEachChild(node, visit);
|
|
239
|
+
SsrScanner.#walk(node, (child) => {
|
|
240
|
+
if (!ts2.isCallExpression(child))
|
|
241
|
+
return;
|
|
242
|
+
const callee = child.expression.getText(sourceFile);
|
|
243
|
+
if (/^fetch\.[a-z]/.test(callee) || /^st\.do\.(init|get|view|load|list|count|insight)[A-Z]/.test(callee))
|
|
244
|
+
calls.push({ callee, node: child });
|
|
245
|
+
});
|
|
240
246
|
return calls;
|
|
241
247
|
}
|
|
242
248
|
#getTemplateStateWarnings(sourceFile) {
|
|
243
249
|
if (!sourceFile.file.endsWith(".Template.tsx"))
|
|
244
250
|
return [];
|
|
245
251
|
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);
|
|
252
|
+
SsrScanner.#walk(sourceFile.sourceFile, (node) => {
|
|
253
|
+
if (ts2.isCallExpression(node) && node.expression.getText(sourceFile.sourceFile) === "useState")
|
|
254
|
+
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)));
|
|
255
|
+
});
|
|
260
256
|
return warnings;
|
|
261
257
|
}
|
|
262
258
|
#measureBalance(sourceFiles) {
|
|
@@ -311,7 +307,7 @@ class SsrScanner {
|
|
|
311
307
|
}
|
|
312
308
|
#getTouches(sourceFile, node) {
|
|
313
309
|
const touches = [];
|
|
314
|
-
|
|
310
|
+
SsrScanner.#walk(node, (child) => {
|
|
315
311
|
if (ts2.isCallExpression(child)) {
|
|
316
312
|
const callee = child.expression.getText(sourceFile);
|
|
317
313
|
const bareName = callee.split(".").pop() ?? callee;
|
|
@@ -329,31 +325,32 @@ class SsrScanner {
|
|
|
329
325
|
else if (SsrScanner.#clientGlobals.has(root))
|
|
330
326
|
touches.push(root);
|
|
331
327
|
}
|
|
332
|
-
|
|
333
|
-
};
|
|
334
|
-
ts2.forEachChild(node, visit);
|
|
328
|
+
});
|
|
335
329
|
return touches;
|
|
336
330
|
}
|
|
337
331
|
#getTagNames(sourceFile, node) {
|
|
338
332
|
const tags = new Set;
|
|
339
|
-
|
|
333
|
+
SsrScanner.#walk(node, (child) => {
|
|
340
334
|
if (ts2.isJsxOpeningElement(child) || ts2.isJsxSelfClosingElement(child))
|
|
341
335
|
tags.add(child.tagName.getText(sourceFile).split(".")[0]);
|
|
342
|
-
|
|
343
|
-
};
|
|
344
|
-
ts2.forEachChild(node, visit);
|
|
336
|
+
});
|
|
345
337
|
return tags;
|
|
346
338
|
}
|
|
347
339
|
#getMass(node) {
|
|
348
340
|
let mass = 0;
|
|
349
|
-
|
|
341
|
+
SsrScanner.#walk(node, (child) => {
|
|
350
342
|
if (ts2.isJsxOpeningElement(child) || ts2.isJsxSelfClosingElement(child))
|
|
351
343
|
mass += 1;
|
|
352
|
-
|
|
353
|
-
};
|
|
354
|
-
ts2.forEachChild(node, visit);
|
|
344
|
+
});
|
|
355
345
|
return mass;
|
|
356
346
|
}
|
|
347
|
+
static #walk(node, visit) {
|
|
348
|
+
const walk = (child) => {
|
|
349
|
+
visit(child);
|
|
350
|
+
ts2.forEachChild(child, walk);
|
|
351
|
+
};
|
|
352
|
+
ts2.forEachChild(node, walk);
|
|
353
|
+
}
|
|
357
354
|
#hasUseClient(sourceFile) {
|
|
358
355
|
const first = sourceFile.statements[0];
|
|
359
356
|
if (!first || !ts2.isExpressionStatement(first) || !ts2.isStringLiteral(first.expression))
|
|
@@ -491,7 +488,7 @@ var RULE_FIXES = {
|
|
|
491
488
|
"akan.global.duplicate-exported-function-body": "Extract the shared implementation into a single exported helper and import it, instead of copying the body.",
|
|
492
489
|
"akan.file.recommended-max-lines": "Split the file by responsibility \u2014 move Zones, Utils, or subcomponents into sibling files.",
|
|
493
490
|
"akan.file.max-lines": "Break the file into smaller focused modules; keep one primary responsibility per file.",
|
|
494
|
-
"akan.file.abstract-max-lines": "
|
|
491
|
+
"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
492
|
"akan.file.placeholder-export": "Remove the placeholder export; generated indexes should only re-export real modules.",
|
|
496
493
|
"akan.file.bang-comment-in-client": "Rewrite the marker as `// FIXME:` or `// TODO:`. Keep the bang form for server, srvkit, and CLI files.",
|
|
497
494
|
"akan.file.dictionary-stale-text": "Replace the scaffold text with real localized copy for this dictionary entry.",
|
|
@@ -549,7 +546,7 @@ class AkanQualityScanner {
|
|
|
549
546
|
const ignoreFilter = ignore().add(await this.#readGitIgnore(workspaceRoot));
|
|
550
547
|
const files = [];
|
|
551
548
|
for (const targetRoot of ["apps", "libs"]) {
|
|
552
|
-
const absoluteTargetRoot =
|
|
549
|
+
const absoluteTargetRoot = path2.join(workspaceRoot, targetRoot);
|
|
553
550
|
if (!await isDirectory(absoluteTargetRoot))
|
|
554
551
|
continue;
|
|
555
552
|
await this.#walkTargetFiles(workspaceRoot, absoluteTargetRoot, ignoreFilter, files);
|
|
@@ -557,7 +554,7 @@ class AkanQualityScanner {
|
|
|
557
554
|
return files.sort();
|
|
558
555
|
}
|
|
559
556
|
async#readGitIgnore(workspaceRoot) {
|
|
560
|
-
const gitIgnorePath =
|
|
557
|
+
const gitIgnorePath = path2.join(workspaceRoot, ".gitignore");
|
|
561
558
|
if (!await Bun.file(gitIgnorePath).exists())
|
|
562
559
|
return [];
|
|
563
560
|
return (await readFile(gitIgnorePath, "utf8")).split(/\r?\n/);
|
|
@@ -565,8 +562,8 @@ class AkanQualityScanner {
|
|
|
565
562
|
async#walkTargetFiles(workspaceRoot, currentPath, ignoreFilter, files) {
|
|
566
563
|
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
567
564
|
for (const entry of entries) {
|
|
568
|
-
const absolutePath =
|
|
569
|
-
const relativePath = toPosix(
|
|
565
|
+
const absolutePath = path2.join(currentPath, entry.name);
|
|
566
|
+
const relativePath = toPosix(path2.relative(workspaceRoot, absolutePath));
|
|
570
567
|
if (shouldSkipPath(relativePath, entry.isDirectory(), ignoreFilter))
|
|
571
568
|
continue;
|
|
572
569
|
if (entry.isDirectory()) {
|
|
@@ -581,7 +578,7 @@ class AkanQualityScanner {
|
|
|
581
578
|
}
|
|
582
579
|
}
|
|
583
580
|
async#readSourceFile(workspaceRoot, file) {
|
|
584
|
-
const absolutePath =
|
|
581
|
+
const absolutePath = path2.join(workspaceRoot, file);
|
|
585
582
|
const content = await readFile(absolutePath, "utf8");
|
|
586
583
|
return {
|
|
587
584
|
file,
|
|
@@ -591,7 +588,7 @@ class AkanQualityScanner {
|
|
|
591
588
|
};
|
|
592
589
|
}
|
|
593
590
|
async#readTextFile(workspaceRoot, file) {
|
|
594
|
-
return { file, content: await readFile(
|
|
591
|
+
return { file, content: await readFile(path2.join(workspaceRoot, file), "utf8") };
|
|
595
592
|
}
|
|
596
593
|
#scanGlobalQuality(sourceFiles) {
|
|
597
594
|
const exportedFunctionLikes = sourceFiles.flatMap((sourceFile) => getExportedFunctionLikes(sourceFile));
|
|
@@ -627,24 +624,10 @@ class AkanQualityScanner {
|
|
|
627
624
|
const warnings = [];
|
|
628
625
|
const lineCount = sourceFile.content.split(/\r?\n/).length;
|
|
629
626
|
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
|
-
}
|
|
627
|
+
if (recommendedLineLimit && lineCount > recommendedLineLimit)
|
|
628
|
+
warnings.push(fileWarning("akan.file.recommended-max-lines", "file", sourceFile.file, `File has ${lineCount} lines. Recommended limit for this file type is ${recommendedLineLimit} lines.`));
|
|
629
|
+
if (lineCount > MAX_FILE_LINES)
|
|
630
|
+
warnings.push(fileWarning("akan.file.max-lines", "file", sourceFile.file, `File has ${lineCount} lines. Keep single files under ${MAX_FILE_LINES} lines.`));
|
|
648
631
|
warnings.push(...getBangCommentWarnings(sourceFile));
|
|
649
632
|
warnings.push(...getPlaceholderExportWarnings(sourceFile));
|
|
650
633
|
warnings.push(...getDictionaryTextWarnings(sourceFile));
|
|
@@ -658,14 +641,7 @@ class AkanQualityScanner {
|
|
|
658
641
|
continue;
|
|
659
642
|
if (declaration.kind === "interface" && allowedInterfaceNames.has(declaration.name))
|
|
660
643
|
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
|
-
});
|
|
644
|
+
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
645
|
}
|
|
670
646
|
return warnings;
|
|
671
647
|
}
|
|
@@ -687,28 +663,13 @@ class AkanQualityScanner {
|
|
|
687
663
|
if (declaration.exported) {
|
|
688
664
|
if (isAllowedComponentExport(declaration, isPage))
|
|
689
665
|
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
|
-
});
|
|
666
|
+
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
667
|
continue;
|
|
699
668
|
}
|
|
700
669
|
if (isComponentValueKind(declaration.kind) && compoundComponentNames.has(declaration.name))
|
|
701
670
|
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
|
-
}
|
|
671
|
+
if (isRestrictedInternalKind(declaration.kind))
|
|
672
|
+
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
673
|
}
|
|
713
674
|
return warnings;
|
|
714
675
|
}
|
|
@@ -716,34 +677,15 @@ class AkanQualityScanner {
|
|
|
716
677
|
const suffix = CONVENTION_SUFFIXES.find((candidate) => sourceFile.file.endsWith(candidate));
|
|
717
678
|
if (!suffix)
|
|
718
679
|
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;
|
|
680
|
+
const modelName = toPascalCase(path2.basename(sourceFile.file, suffix));
|
|
681
|
+
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
682
|
}
|
|
735
683
|
#scanAbstractQuality({ file, content }) {
|
|
736
684
|
const lineCount = AbstractDoc.lineCountOf(content);
|
|
737
685
|
if (lineCount <= AbstractDoc.maxLines)
|
|
738
686
|
return [];
|
|
739
687
|
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
|
-
}
|
|
688
|
+
fileWarning("akan.file.abstract-max-lines", "file", file, `Abstract has ${lineCount} lines. Keep abstracts under ${AbstractDoc.maxLines} lines and compact them periodically.`)
|
|
747
689
|
];
|
|
748
690
|
}
|
|
749
691
|
#scanLayoutQuality(sourceFile) {
|
|
@@ -753,26 +695,12 @@ class AkanQualityScanner {
|
|
|
753
695
|
const { type, name, isDir } = rootEntry;
|
|
754
696
|
const allowed = isDir ? rootAllowedDirs[type].has(name) : rootAllowedFiles[type].has(name);
|
|
755
697
|
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
|
-
}
|
|
698
|
+
if (!allowed)
|
|
699
|
+
warnings.push(fileWarning(`akan.layout.${type}-root-${kind}`, "layout", sourceFile.file, `Unexpected ${type} root ${kind} "${name}". Keep ${type} code in conventional ${type} folders.`));
|
|
765
700
|
}
|
|
766
701
|
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
|
-
}
|
|
702
|
+
if (libFacetFile && !isAllowedLibFacetRootFile(libFacetFile))
|
|
703
|
+
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
704
|
const moduleUiWarning = getModuleUiWarning(sourceFile.file);
|
|
777
705
|
if (moduleUiWarning)
|
|
778
706
|
warnings.push(moduleUiWarning);
|
|
@@ -839,67 +767,48 @@ function formatQualityLocation(file, line) {
|
|
|
839
767
|
function getExportedFunctionLikes(sourceFile) {
|
|
840
768
|
const declarations = [];
|
|
841
769
|
const nameExempt = isPageRouteFile(sourceFile.file) || isUiComponentFile(sourceFile.file);
|
|
770
|
+
const add = (name, kind, node, body, isEnumClass = false) => declarations.push({
|
|
771
|
+
name,
|
|
772
|
+
kind,
|
|
773
|
+
file: sourceFile.file,
|
|
774
|
+
line: getLine(sourceFile.sourceFile, node),
|
|
775
|
+
bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, body),
|
|
776
|
+
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClass)
|
|
777
|
+
});
|
|
842
778
|
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
|
-
}
|
|
779
|
+
if (ts3.isFunctionDeclaration(statement) && statement.name && isExported(statement))
|
|
780
|
+
add(statement.name.text, "function", statement, statement.body);
|
|
781
|
+
if (ts3.isClassDeclaration(statement) && statement.name && isExported(statement))
|
|
782
|
+
add(statement.name.text, "class", statement, statement, isEnumClassStatement(sourceFile.sourceFile, statement));
|
|
863
783
|
if (ts3.isVariableStatement(statement) && isExported(statement)) {
|
|
864
784
|
for (const declaration of statement.declarationList.declarations) {
|
|
865
785
|
if (!ts3.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
|
|
866
786
|
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
|
-
});
|
|
787
|
+
add(declaration.name.text, "function-variable", declaration, declaration.initializer);
|
|
875
788
|
}
|
|
876
789
|
}
|
|
877
790
|
}
|
|
878
791
|
return declarations;
|
|
879
792
|
}
|
|
880
|
-
function
|
|
793
|
+
function sysSegments(file) {
|
|
881
794
|
const segments = file.split("/");
|
|
882
|
-
return
|
|
795
|
+
return segments[0] === "apps" || segments[0] === "libs" ? segments : null;
|
|
796
|
+
}
|
|
797
|
+
function isPageRouteFile(file) {
|
|
798
|
+
return sysSegments(file)?.[2] === "page";
|
|
883
799
|
}
|
|
884
800
|
function isUiComponentFile(file) {
|
|
885
|
-
|
|
886
|
-
return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "ui";
|
|
801
|
+
return sysSegments(file)?.[2] === "ui";
|
|
887
802
|
}
|
|
888
803
|
function isConventionDuplicateNameExempt(file, isEnumClass) {
|
|
889
|
-
if (!
|
|
804
|
+
if (!sysSegments(file)?.includes("lib"))
|
|
890
805
|
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"))
|
|
806
|
+
if (file.endsWith(".tsx") || /\.(document|service|signal|store)\.ts$/.test(file))
|
|
894
807
|
return true;
|
|
895
808
|
if (file.endsWith(".constant.ts"))
|
|
896
809
|
return !isEnumClass;
|
|
897
810
|
return false;
|
|
898
811
|
}
|
|
899
|
-
function isInLibModule(file) {
|
|
900
|
-
const segments = file.split("/");
|
|
901
|
-
return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
|
|
902
|
-
}
|
|
903
812
|
function isEnumClassStatement(sourceFile, statement) {
|
|
904
813
|
if (!ts3.isClassDeclaration(statement))
|
|
905
814
|
return false;
|
|
@@ -995,14 +904,7 @@ function isDefaultExportStatement(statement) {
|
|
|
995
904
|
function getBangCommentWarnings(sourceFile) {
|
|
996
905
|
if (!isClientReachableFile(sourceFile.file))
|
|
997
906
|
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
|
-
}));
|
|
907
|
+
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
908
|
}
|
|
1007
909
|
function isClientReachableFile(file) {
|
|
1008
910
|
if (/\.(test|spec)\.tsx?$/.test(file))
|
|
@@ -1021,61 +923,24 @@ function isClientReachableFile(file) {
|
|
|
1021
923
|
function getPlaceholderExportWarnings(sourceFile) {
|
|
1022
924
|
if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx"))
|
|
1023
925
|
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
|
-
}));
|
|
926
|
+
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
927
|
}
|
|
1033
928
|
function getDictionaryTextWarnings(sourceFile) {
|
|
1034
929
|
if (!sourceFile.file.endsWith(".dictionary.ts"))
|
|
1035
930
|
return [];
|
|
1036
931
|
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
|
-
})));
|
|
932
|
+
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
933
|
}
|
|
1046
934
|
function getGlobalMutationWarnings(sourceFile) {
|
|
1047
935
|
const warnings = [];
|
|
1048
936
|
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
|
-
}
|
|
937
|
+
const warn = (rule, message) => warnings.push(fileWarning(rule, "file", sourceFile.file, message, getLine(sourceFile.sourceFile, statement)));
|
|
938
|
+
if (ts3.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global")
|
|
939
|
+
warn("akan.file.global-declaration", "Global declarations require an explicit low-level integration allowlist.");
|
|
940
|
+
if (ts3.isInterfaceDeclaration(statement) && statement.name.text === "Window")
|
|
941
|
+
warn("akan.file.window-augmentation", "Window augmentation should be isolated to approved browser integration files.");
|
|
942
|
+
if (ts3.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype."))
|
|
943
|
+
warn("akan.file.prototype-mutation", "Prototype mutation should be avoided or isolated to approved low-level integration files.");
|
|
1079
944
|
}
|
|
1080
945
|
return warnings;
|
|
1081
946
|
}
|
|
@@ -1084,73 +949,59 @@ function getTopLevelDeclarations(sourceFile) {
|
|
|
1084
949
|
}
|
|
1085
950
|
function getTopLevelDeclaration(sourceFile, statement) {
|
|
1086
951
|
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)) {
|
|
952
|
+
const declared = (name, kind, at = line) => ({
|
|
953
|
+
name,
|
|
954
|
+
kind,
|
|
955
|
+
line: at,
|
|
956
|
+
exported: isExported(statement),
|
|
957
|
+
node: statement
|
|
958
|
+
});
|
|
959
|
+
if (ts3.isClassDeclaration(statement) && statement.name)
|
|
960
|
+
return [declared(statement.name.text, "class")];
|
|
961
|
+
if (ts3.isFunctionDeclaration(statement) && statement.name)
|
|
962
|
+
return [declared(statement.name.text, "function")];
|
|
963
|
+
if (ts3.isInterfaceDeclaration(statement))
|
|
964
|
+
return [declared(statement.name.text, "interface")];
|
|
965
|
+
if (ts3.isTypeAliasDeclaration(statement))
|
|
966
|
+
return [declared(statement.name.text, "type")];
|
|
967
|
+
if (ts3.isEnumDeclaration(statement))
|
|
968
|
+
return [declared(statement.name.text, "enum")];
|
|
969
|
+
if (ts3.isVariableStatement(statement))
|
|
970
|
+
return statement.declarationList.declarations.filter((declaration) => ts3.isIdentifier(declaration.name)).map((declaration) => declared(declaration.name.text, "variable", getLine(sourceFile, declaration)));
|
|
971
|
+
if (ts3.isExportDeclaration(statement))
|
|
1112
972
|
return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
|
|
1113
|
-
}
|
|
1114
973
|
return [];
|
|
1115
974
|
}
|
|
1116
|
-
function
|
|
1117
|
-
if (suffix === ".dictionary.ts")
|
|
1118
|
-
return isExportedConst(declaration) && declaration.name === "dictionary";
|
|
975
|
+
function getConventionClassNames(suffix, model) {
|
|
1119
976
|
if (suffix === ".constant.ts")
|
|
1120
|
-
return
|
|
977
|
+
return [`${model}Input`, `${model}Object`, model, `Light${model}`, `${model}Insight`];
|
|
1121
978
|
if (suffix === ".document.ts")
|
|
1122
|
-
return
|
|
979
|
+
return [`${model}Filter`, model, `${model}Model`];
|
|
1123
980
|
if (suffix === ".service.ts")
|
|
1124
|
-
return
|
|
981
|
+
return [`${model}Service`];
|
|
1125
982
|
if (suffix === ".signal.ts")
|
|
1126
|
-
return
|
|
983
|
+
return [`${model}Internal`, `${model}Slice`, `${model}Endpoint`];
|
|
1127
984
|
if (suffix === ".store.ts")
|
|
1128
|
-
return
|
|
1129
|
-
return
|
|
985
|
+
return [`${model}Store`];
|
|
986
|
+
return [];
|
|
1130
987
|
}
|
|
1131
|
-
function
|
|
988
|
+
function isAllowedConventionDeclaration(suffix, modelName, declaration) {
|
|
989
|
+
if (suffix === ".dictionary.ts")
|
|
990
|
+
return isExportedConst(declaration) && declaration.name === "dictionary";
|
|
1132
991
|
if (declaration.kind !== "class")
|
|
1133
992
|
return false;
|
|
1134
|
-
if (
|
|
993
|
+
if (getConventionClassNames(suffix, modelName).includes(declaration.name))
|
|
1135
994
|
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(");
|
|
995
|
+
return suffix === ".constant.ts" && isEnumClassStatement(declaration.node.getSourceFile(), declaration.node);
|
|
1141
996
|
}
|
|
1142
997
|
function getConventionDescription(suffix, modelName) {
|
|
1143
998
|
if (suffix === ".dictionary.ts")
|
|
1144
999
|
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`;
|
|
1000
|
+
const names = [
|
|
1001
|
+
...getConventionClassNames(suffix, modelName),
|
|
1002
|
+
...suffix === ".constant.ts" ? ["enumOf classes"] : []
|
|
1003
|
+
];
|
|
1004
|
+
return names.length > 1 ? `${names.slice(0, -1).join(", ")}, or ${names.at(-1)}` : names[0];
|
|
1154
1005
|
}
|
|
1155
1006
|
var sysRootTypes = { apps: "app", libs: "lib" };
|
|
1156
1007
|
function getSysRootEntry(file) {
|
|
@@ -1182,13 +1033,7 @@ function getModuleUiWarning(file) {
|
|
|
1182
1033
|
const allowedFileNames = new Set(allowedSuffixes.map((suffix) => `${pascalName}.${suffix}.tsx`));
|
|
1183
1034
|
if (allowedFileNames.has(fileName) || fileName.endsWith(".test.tsx") || fileName.endsWith(".spec.tsx"))
|
|
1184
1035
|
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
|
-
};
|
|
1036
|
+
return fileWarning("akan.layout.module-ui-file", "layout", file, `Unexpected ${kind} module UI filename "${fileName}". Expected one of: ${[...allowedFileNames].join(", ")}.`);
|
|
1192
1037
|
}
|
|
1193
1038
|
function getModuleInfo(file) {
|
|
1194
1039
|
const segments = file.split("/");
|
|
@@ -1204,10 +1049,8 @@ function getModuleInfo(file) {
|
|
|
1204
1049
|
if (segments.length !== libIndex + 3)
|
|
1205
1050
|
return null;
|
|
1206
1051
|
const fileName = segments[libIndex + 2];
|
|
1207
|
-
if (moduleName.startsWith("__"))
|
|
1208
|
-
|
|
1209
|
-
return { moduleName: scalarName, fileName, kind: "scalar" };
|
|
1210
|
-
}
|
|
1052
|
+
if (moduleName.startsWith("__"))
|
|
1053
|
+
return { moduleName, fileName, kind: "scalar" };
|
|
1211
1054
|
if (moduleName.startsWith("_"))
|
|
1212
1055
|
return { moduleName, fileName, kind: "service" };
|
|
1213
1056
|
return { moduleName, fileName, kind: "database" };
|
|
@@ -1216,7 +1059,7 @@ function isExportedConst(declaration) {
|
|
|
1216
1059
|
return declaration.exported && ts3.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts3.NodeFlags.Const) !== 0;
|
|
1217
1060
|
}
|
|
1218
1061
|
function isExported(node) {
|
|
1219
|
-
return !!
|
|
1062
|
+
return !!(ts3.getCombinedModifierFlags(node) & ts3.ModifierFlags.Export);
|
|
1220
1063
|
}
|
|
1221
1064
|
function isFunctionLikeInitializer(node) {
|
|
1222
1065
|
return !!node && (ts3.isArrowFunction(node) || ts3.isFunctionExpression(node));
|
|
@@ -1262,7 +1105,7 @@ function toPascalCase(value) {
|
|
|
1262
1105
|
return value.replace(/(^|[-_./])([a-zA-Z0-9])/g, (_, __, char) => char.toUpperCase()).replace(/[-_./]/g, "");
|
|
1263
1106
|
}
|
|
1264
1107
|
function toPosix(value) {
|
|
1265
|
-
return value.split(
|
|
1108
|
+
return value.split(path2.sep).join("/");
|
|
1266
1109
|
}
|
|
1267
1110
|
function groupBy(items, getKey) {
|
|
1268
1111
|
const grouped = new Map;
|
|
@@ -1279,24 +1122,16 @@ function compareWarnings(a, b) {
|
|
|
1279
1122
|
// pkgs/@akanjs/cli/quality/quality.script.ts
|
|
1280
1123
|
import { Logger } from "akanjs/common";
|
|
1281
1124
|
|
|
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]) {
|
|
1125
|
+
class QualityScript extends script("quality") {
|
|
1291
1126
|
async scan(workspace, format = "text") {
|
|
1292
1127
|
const spinner = workspace.spinning("Scanning Akan code quality...");
|
|
1293
|
-
const result = await
|
|
1128
|
+
const result = await new AkanQualityScanner().scan(workspace.workspaceRoot);
|
|
1294
1129
|
spinner.succeed(`Quality scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
|
|
1295
1130
|
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatQualityScanResult(result));
|
|
1296
1131
|
}
|
|
1297
1132
|
async ssr(workspace, format = "text") {
|
|
1298
1133
|
const spinner = workspace.spinning("Measuring Akan server/client render balance...");
|
|
1299
|
-
const scanned = await
|
|
1134
|
+
const scanned = await new AkanQualityScanner().scan(workspace.workspaceRoot);
|
|
1300
1135
|
const result = { ...scanned, warnings: scanned.warnings.filter((warning) => warning.scope === "ssr") };
|
|
1301
1136
|
spinner.succeed(`SSR scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
|
|
1302
1137
|
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSsrScanResult(result));
|
|
@@ -1305,15 +1140,7 @@ class QualityScript extends script("quality", [QualityRunner]) {
|
|
|
1305
1140
|
|
|
1306
1141
|
// pkgs/@akanjs/cli/quality/quality.command.ts
|
|
1307
1142
|
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) {
|
|
1143
|
+
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
1144
|
if (action === "scan")
|
|
1318
1145
|
await this.qualityScript.scan(workspace, format);
|
|
1319
1146
|
else if (action === "ssr")
|