@akanjs/cli 3.0.0-alpha.11 → 3.0.0-alpha.110
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/README.ko.md +1 -1
- package/README.md +1 -1
- package/{abstractCompactor-e5yq60qz.js → abstractCompactor-mjh5ppsq.js} +3 -5
- package/{agent.command-a3n15c6t.js → agent.command-d2gfxfck.js} +11 -13
- package/{application.command-rdm3et0g.js → application.command-pj2m7584.js} +65 -38
- package/{applicationBuildRunner-6m8n71w4.js → applicationBuildRunner-kxwbssgn.js} +27 -23
- package/{applicationReleasePackager-gg8ccbeh.js → applicationReleasePackager-4yasxwv9.js} +8 -9
- package/buildBatch.proc.js +12 -14
- package/capacitorApp-36rbr858.js +12 -0
- package/{cloud.command-56aeekqn.js → cloud.command-gxqp8xdv.js} +15 -18
- package/commandManifest.json +1 -1
- package/{context.command-ysxawe7h.js → context.command-ez8a0zyz.js} +19 -23
- package/dependencyScanner-shs82ynp.js +8 -0
- package/devTui-40ggmd99.js +553 -0
- package/devTuiView-c08ccbat.js +9 -0
- package/getDirname-dr3jz1xg.js +7 -0
- package/{guideline.command-yqf9a372.js → guideline.command-xmqwx8zm.js} +24 -22
- package/guidelines/agentRule/agentRule.generate.json +31 -0
- package/guidelines/agentRule/agentRule.instruction.md +531 -0
- package/guidelines/conventions/conventions.instruction.md +493 -369
- package/guidelines/framework/framework.generate.json +3 -2
- package/guidelines/framework/framework.instruction.md +0 -9
- package/guidelines/mcpRule/mcpRule.generate.json +35 -0
- package/guidelines/mcpRule/mcpRule.instruction.md +460 -0
- package/guidelines/modelStore/modelStore.instruction.md +12 -0
- package/guidelines/modelTemplate/modelTemplate.instruction.md +5 -0
- package/guidelines/queryRule/queryRule.generate.json +31 -0
- package/guidelines/queryRule/queryRule.instruction.md +178 -0
- package/guidelines/runtimeRule/runtimeRule.generate.json +25 -0
- package/guidelines/runtimeRule/runtimeRule.instruction.md +220 -0
- package/guidelines/ssrRule/ssrRule.instruction.md +51 -5
- package/guidelines/transportRule/transportRule.generate.json +31 -0
- package/guidelines/transportRule/transportRule.instruction.md +60 -0
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +54 -764
- package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +33 -0
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +467 -0
- package/incrementalBuilder.proc.js +26 -25
- package/{index-8pd02prk.js → index-259dsnz3.js} +58 -25
- package/{index-yzsxc30b.js → index-2tv3fdx4.js} +975 -234
- package/index-3d9y066c.js +76 -0
- package/{index-76rn3g2c.js → index-47pj3aee.js} +4 -4
- package/{index-m90fn8xe.js → index-4edzf1gr.js} +32 -32
- package/{index-3789fdpg.js → index-600zhnve.js} +94 -61
- package/{index-1577bej2.js → index-6wwzs1bc.js} +2 -2
- package/{index-cv9cxs5k.js → index-7737gxcp.js} +18 -18
- package/{index-a5cs7d8r.js → index-8few9nyh.js} +13 -13
- package/{index-mewacxye.js → index-8pm30m73.js} +57 -57
- package/{index-wnp7hwq7.js → index-92zztqy1.js} +7 -7
- package/{index-fgc8r6dj.js → index-a5vmm6x4.js} +3 -1
- package/{index-cav1azma.js → index-a64esmtb.js} +1106 -1369
- package/{index-nwqqssnb.js → index-aj7tyh5w.js} +26 -23
- package/{index-xys926f2.js → index-by5gpp8x.js} +4 -4
- package/{index-yzmqvg23.js → index-fz5q3gd7.js} +8 -8
- package/{index-kcsa1zff.js → index-gyd220p2.js} +4 -4
- package/{index-j37qq1f2.js → index-h7yw2z9n.js} +7 -7
- package/{index-bjpxzr6s.js → index-hwkygn9s.js} +51 -3
- package/index-j9yhg404.js +159 -0
- package/{index-46tjzh6s.js → index-kcxmv48c.js} +29 -29
- package/{index-0cj2zxbm.js → index-kn1h7p1x.js} +12 -11
- package/{index-e4b2ae6b.js → index-ktb5njgy.js} +41 -34
- package/{index-etwh7sd3.js → index-mak3cd28.js} +100 -32
- package/{index-xjd3rqzj.js → index-n68jx79w.js} +6 -15
- package/{index-z1s5f4e0.js → index-ne2qy0nk.js} +36 -37
- package/{index-65aeh789.js → index-pnqbkf5p.js} +1 -1
- package/{index-67546d0j.js → index-q0y2jq9j.js} +14 -2
- package/{index-4mgkgba3.js → index-rwg2aa4f.js} +14 -14
- package/{index-bzznxgcx.js → index-v0qh106m.js} +1002 -128
- package/{index-j9cxndc0.js → index-w8kmx1b6.js} +20 -11
- package/{index-h5whaj26.js → index-ws5e0j51.js} +16 -12
- package/{index-9va8zayb.js → index-wt7qrtd4.js} +27 -27
- package/index.js +22 -25
- package/{library.command-x6t29a30.js → library.command-nbsky672.js} +11 -10
- package/{localRegistry.command-649g1n7b.js → localRegistry.command-ts6t5e5a.js} +20 -23
- package/{module.command-8w8ys5t3.js → module.command-29vqzrh8.js} +13 -16
- package/{package.command-zgy7f3fz.js → package.command-axv0abmn.js} +8 -10
- package/package.json +5 -7
- package/{page.command-yjmnkyk2.js → page.command-vs9s3zrh.js} +7 -9
- package/{primitive.command-gfqaagsg.js → primitive.command-cbxmqbtc.js} +13 -16
- package/{quality.command-ec8r1dn3.js → quality.command-gmbs7nws.js} +184 -334
- package/recipeScanner-fpagtgc1.js +11 -0
- package/{repair.command-p9ey6mkm.js → repair.command-bqsjptsj.js} +12 -14
- package/{routeSourceValidator-e4jmbfcx.js → routeSourceValidator-apyqw7wk.js} +90 -19
- package/{scalar.command-9dhmqhtm.js → scalar.command-6k1vhe9s.js} +11 -14
- package/subspace.command-35a3t6vv.js +755 -0
- package/templates/app/env/env.client.debug.ts.template +1 -5
- package/templates/app/env/env.client.develop.ts.template +1 -5
- package/templates/app/env/env.client.local.ts.template +1 -5
- package/templates/app/env/env.client.main.ts.template +1 -5
- package/templates/app/env/env.client.testing.ts.template +1 -5
- package/templates/app/env/env.client.type.ts +2 -6
- package/templates/app/env/env.server.debug.ts.template +1 -5
- package/templates/app/env/env.server.develop.ts.template +1 -5
- package/templates/app/env/env.server.local.ts.template +1 -5
- package/templates/app/env/env.server.main.ts.template +1 -5
- package/templates/app/main.ts +3 -1
- package/templates/app/page/_index.tsx +3 -3
- package/templates/app/page/_layout.tsx +31 -32
- package/templates/appSample/lib/task/task.signal.ts +11 -6
- package/templates/appSample/page/task/[taskId]/_index.tsx +22 -27
- package/templates/appSample/page/task/[taskId]/edit.tsx +25 -30
- package/templates/appSample/page/task/_index.tsx +3 -7
- package/templates/appSample/page/task/_layout.tsx +4 -15
- package/templates/appSample/page/task/new.tsx +4 -10
- package/templates/appSample/srvkit/AuthGuard.ts +2 -2
- package/templates/crudPages/[__model__Id]/edit/page.tsx +27 -29
- package/templates/crudPages/[__model__Id]/page.tsx +24 -32
- package/templates/crudPages/new/page.tsx +26 -25
- package/templates/crudPages/page.tsx +27 -26
- package/templates/crudSinglePage/page.tsx +26 -21
- package/templates/env/_env.server.type.ts +7 -5
- package/templates/lib/db.ts +1 -1
- package/templates/lib/dict.ts +1 -1
- package/templates/lib/sig.ts +5 -1
- package/templates/lib/srv.ts +6 -2
- package/templates/module/__model__.dictionary.ts +4 -1
- package/templates/module/__model__.signal.ts +2 -3
- package/templates/module/__model__.store.ts +1 -2
- package/templates/workspaceRoot/.gitignore.template +0 -1
- package/templates/workspaceRoot/.husky/pre-commit.template +16 -0
- package/templates/workspaceRoot/.vscode/settings.json.template +1 -1
- package/templates/workspaceRoot/AGENTS.md.template +4 -0
- package/templates/workspaceRoot/CLAUDE.md.template +19 -0
- package/templates/workspaceRoot/biome.json.template +1 -1
- package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +10 -14
- package/templates/workspaceRoot/docs/GENERATED.md.template +12 -10
- package/templates/workspaceRoot/package.json.template +2 -4
- package/typeChecker-3xgrzc2v.js +7 -0
- package/typecheck.proc.js +4 -5
- package/{workflow.command-ycwc6zr6.js → workflow.command-nwpcdc8p.js} +22 -25
- package/{workspace.command-xhyhajxa.js → workspace.command-aczjxzbf.js} +64 -66
- package/capacitorApp-mr81qdxq.js +0 -61
- package/dependencyScanner-f6grrep3.js +0 -9
- package/getDirname-m9s0cmhq.js +0 -8
- package/index-mxvakhsm.js +0 -66
- package/index-r24hmh0q.js +0 -4
- package/recipeScanner-tghkzrrt.js +0 -12
- package/typeChecker-kravn7ns.js +0 -8
- /package/{index-0wae5ebk.js → index-meh558jy.js} +0 -0
- /package/{index-ss469dec.js → index-nz8prx9g.js} +0 -0
|
@@ -1,177 +1,95 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import {
|
|
3
|
-
AbstractDoc
|
|
4
|
-
} from "./index-0wae5ebk.js";
|
|
5
2
|
import {
|
|
6
3
|
Workspace,
|
|
7
|
-
command,
|
|
8
4
|
runner,
|
|
9
|
-
script
|
|
10
|
-
|
|
5
|
+
script,
|
|
6
|
+
command
|
|
7
|
+
} from "./index-mak3cd28.js";
|
|
8
|
+
import"./index-q0y2jq9j.js";
|
|
11
9
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import"./index-
|
|
17
|
-
import
|
|
18
|
-
|
|
10
|
+
rootAllowedFiles,
|
|
11
|
+
rootAllowedDirs,
|
|
12
|
+
isAllowedLibFacetRootFile
|
|
13
|
+
} from "./index-2tv3fdx4.js";
|
|
14
|
+
import"./index-6wwzs1bc.js";
|
|
15
|
+
import {
|
|
16
|
+
AbstractDoc
|
|
17
|
+
} from "./index-meh558jy.js";
|
|
19
18
|
|
|
20
19
|
// pkgs/@akanjs/devkit/qualityScanner.ts
|
|
21
20
|
import { createHash } from "crypto";
|
|
22
21
|
import { readdir, readFile, stat } from "fs/promises";
|
|
23
|
-
import
|
|
22
|
+
import path from "path";
|
|
24
23
|
import { RESERVED_ROUTE_CONFIG_EXPORTS } from "akanjs/common";
|
|
25
24
|
import ignore from "ignore";
|
|
26
|
-
import
|
|
25
|
+
import ts3 from "typescript";
|
|
27
26
|
|
|
28
|
-
// pkgs/@akanjs/devkit/
|
|
29
|
-
import path from "path";
|
|
27
|
+
// pkgs/@akanjs/devkit/formSetterScanner.ts
|
|
30
28
|
import ts from "typescript";
|
|
31
29
|
|
|
32
|
-
class
|
|
30
|
+
class FormSetterScanner {
|
|
31
|
+
static #fieldSetter = /^set[A-Za-z0-9_$]*On[A-Za-z0-9_$]*$/;
|
|
33
32
|
scan(sourceFiles) {
|
|
34
|
-
|
|
35
|
-
return sourceFiles.filter((sourceFile) => sourceFile.file.endsWith(".signal.ts")).flatMap((sourceFile) => this.#scanSignal(sourceFile, dictionaries.get(path.dirname(sourceFile.file))));
|
|
36
|
-
}
|
|
37
|
-
#scanSignal(signal, dictionary) {
|
|
38
|
-
const exposed = McpScanner.#exposedDeclarations(signal);
|
|
39
|
-
if (!exposed.length)
|
|
40
|
-
return [];
|
|
41
|
-
const refName = path.basename(signal.file, ".signal.ts").replace(/^_+/, "");
|
|
42
|
-
const described = dictionary ? McpScanner.#describedEntries(dictionary) : new Map;
|
|
43
|
-
return [
|
|
44
|
-
...exposed.filter(({ name, kind }) => !McpScanner.#isDescribed(described, refName, name, kind)).map(({ name, kind, line }) => ({
|
|
45
|
-
rule: "akan.mcp.missing-description",
|
|
46
|
-
scope: "mcp",
|
|
47
|
-
severity: "warning",
|
|
48
|
-
file: signal.file,
|
|
49
|
-
line,
|
|
50
|
-
message: `MCP-exposed ${kind} "${name}" has no dictionary .desc(); an agent sees its name and nothing else.`
|
|
51
|
-
})),
|
|
52
|
-
...exposed.filter(({ guards }) => guards === "missing").map(({ name, kind, line }) => ({
|
|
53
|
-
rule: "akan.mcp.unguarded-exposure",
|
|
54
|
-
scope: "mcp",
|
|
55
|
-
severity: "warning",
|
|
56
|
-
file: signal.file,
|
|
57
|
-
line,
|
|
58
|
-
message: `MCP-exposed ${kind} "${name}" declares no guards; a slice's guards map never reaches a named slice.`
|
|
59
|
-
}))
|
|
60
|
-
];
|
|
61
|
-
}
|
|
62
|
-
static #isDescribed(described, refName, name, kind) {
|
|
63
|
-
if (described.get(kind)?.has(name))
|
|
64
|
-
return true;
|
|
65
|
-
return kind === "slice" && !!described.get("endpoint")?.has(`${refName}List${McpScanner.#capitalize(name)}`);
|
|
33
|
+
return sourceFiles.filter((sourceFile) => sourceFile.file.endsWith(".tsx")).flatMap((f) => this.#scanFile(f));
|
|
66
34
|
}
|
|
67
|
-
|
|
68
|
-
const
|
|
35
|
+
#scanFile({ file, sourceFile }) {
|
|
36
|
+
const wrapped = [];
|
|
69
37
|
const visit = (node) => {
|
|
70
|
-
if (
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
73
|
-
|
|
38
|
+
if (ts.isJsxAttribute(node)) {
|
|
39
|
+
const setter = FormSetterScanner.#wrappedSetterOf(node);
|
|
40
|
+
if (setter)
|
|
41
|
+
wrapped.push({ setter, line: FormSetterScanner.#lineOf(sourceFile, node) });
|
|
74
42
|
}
|
|
75
43
|
ts.forEachChild(node, visit);
|
|
76
44
|
};
|
|
77
|
-
visit(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (!declaration && ts.isPropertyAssignment(node)) {
|
|
91
|
-
declaration = node;
|
|
92
|
-
continue;
|
|
45
|
+
visit(sourceFile);
|
|
46
|
+
if (!wrapped.length)
|
|
47
|
+
return [];
|
|
48
|
+
const setters = [...new Set(wrapped.map((entry) => entry.setter))].sort();
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
rule: "akan.agent.unpublished-form-setter",
|
|
52
|
+
scope: "agent",
|
|
53
|
+
severity: "warning",
|
|
54
|
+
message: setters.length === 1 ? `${setters[0]} is reached through a wrapper, so that field publishes no agent tool and carries no data-akan-action.` : `${setters.length} field setters are reached through a wrapper, so those fields publish no agent tool and carry no data-akan-action: ${setters.join(", ")}.`,
|
|
55
|
+
file,
|
|
56
|
+
line: wrapped[0]?.line,
|
|
57
|
+
locations: wrapped.map(({ line }) => ({ file, line }))
|
|
93
58
|
}
|
|
94
|
-
|
|
95
|
-
if (!kind || !declaration)
|
|
96
|
-
continue;
|
|
97
|
-
const name = McpScanner.#propertyName(declaration);
|
|
98
|
-
if (!name)
|
|
99
|
-
return null;
|
|
100
|
-
const line = sourceFile.getLineAndCharacterOfPosition(declaration.getStart(sourceFile)).line + 1;
|
|
101
|
-
return { name, kind, line, guards: McpScanner.#guardState(option) };
|
|
102
|
-
}
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
static #guardState(option) {
|
|
106
|
-
const options = option.parent;
|
|
107
|
-
if (!ts.isObjectLiteralExpression(options))
|
|
108
|
-
return "unknown";
|
|
109
|
-
if (options.properties.some((property) => ts.isSpreadAssignment(property)))
|
|
110
|
-
return "unknown";
|
|
111
|
-
return options.properties.some((property) => ts.isPropertyAssignment(property) && McpScanner.#propertyName(property) === "guards") ? "declared" : "missing";
|
|
59
|
+
];
|
|
112
60
|
}
|
|
113
|
-
static #
|
|
114
|
-
if (!ts.
|
|
61
|
+
static #wrappedSetterOf(attribute) {
|
|
62
|
+
if (!ts.isIdentifier(attribute.name) || !/^on[A-Z]/.test(attribute.name.text))
|
|
115
63
|
return null;
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
64
|
+
const initializer = attribute.initializer;
|
|
65
|
+
if (!initializer || !ts.isJsxExpression(initializer) || !initializer.expression)
|
|
66
|
+
return null;
|
|
67
|
+
const handler = initializer.expression;
|
|
68
|
+
if (!ts.isArrowFunction(handler) && !ts.isFunctionExpression(handler))
|
|
69
|
+
return null;
|
|
70
|
+
if (!handler.parameters.length)
|
|
71
|
+
return null;
|
|
72
|
+
return FormSetterScanner.#setterCallIn(handler.body);
|
|
73
|
+
}
|
|
74
|
+
static #setterCallIn(node) {
|
|
75
|
+
let found = null;
|
|
76
|
+
const visit = (current) => {
|
|
77
|
+
if (found)
|
|
78
|
+
return;
|
|
79
|
+
if (ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression)) {
|
|
80
|
+
const { expression, name } = current.expression;
|
|
81
|
+
if (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.expression) && expression.expression.text === "st" && expression.name.text === "do" && FormSetterScanner.#fieldSetter.test(name.text)) {
|
|
82
|
+
found = name.text;
|
|
83
|
+
return;
|
|
130
84
|
}
|
|
131
85
|
}
|
|
132
|
-
ts.forEachChild(
|
|
86
|
+
ts.forEachChild(current, visit);
|
|
133
87
|
};
|
|
134
|
-
visit(
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
static #dictionaryStage(node) {
|
|
138
|
-
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression))
|
|
139
|
-
return null;
|
|
140
|
-
const stage = node.expression.name.text;
|
|
141
|
-
return stage === "endpoint" || stage === "slice" ? stage : null;
|
|
142
|
-
}
|
|
143
|
-
static #stageEntries(stage) {
|
|
144
|
-
const callback = stage.arguments[0];
|
|
145
|
-
if (!callback || !ts.isArrowFunction(callback))
|
|
146
|
-
return [];
|
|
147
|
-
const body = ts.isParenthesizedExpression(callback.body) ? callback.body.expression : callback.body;
|
|
148
|
-
if (!ts.isObjectLiteralExpression(body))
|
|
149
|
-
return [];
|
|
150
|
-
return body.properties.flatMap((property) => {
|
|
151
|
-
if (!ts.isPropertyAssignment(property))
|
|
152
|
-
return [];
|
|
153
|
-
const name = McpScanner.#propertyName(property);
|
|
154
|
-
return name ? [[name, McpScanner.#chainCalls(property.initializer)]] : [];
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
static #chainCalls(expression) {
|
|
158
|
-
const calls = new Set;
|
|
159
|
-
let current = expression;
|
|
160
|
-
while (ts.isCallExpression(current) || ts.isPropertyAccessExpression(current)) {
|
|
161
|
-
if (ts.isPropertyAccessExpression(current))
|
|
162
|
-
calls.add(current.name.text);
|
|
163
|
-
current = current.expression;
|
|
164
|
-
}
|
|
165
|
-
return calls;
|
|
166
|
-
}
|
|
167
|
-
static #propertyName(property) {
|
|
168
|
-
const { name } = property;
|
|
169
|
-
if (ts.isIdentifier(name) || ts.isStringLiteral(name))
|
|
170
|
-
return name.text;
|
|
171
|
-
return null;
|
|
88
|
+
visit(node);
|
|
89
|
+
return found;
|
|
172
90
|
}
|
|
173
|
-
static #
|
|
174
|
-
return
|
|
91
|
+
static #lineOf(sourceFile, node) {
|
|
92
|
+
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
175
93
|
}
|
|
176
94
|
}
|
|
177
95
|
|
|
@@ -535,122 +453,6 @@ function getAccessRoot(node) {
|
|
|
535
453
|
return ts2.isIdentifier(current) ? current.text : "";
|
|
536
454
|
}
|
|
537
455
|
|
|
538
|
-
// pkgs/@akanjs/devkit/storeScanner.ts
|
|
539
|
-
import path2 from "path";
|
|
540
|
-
import ts3 from "typescript";
|
|
541
|
-
|
|
542
|
-
class StoreScanner {
|
|
543
|
-
scan(sourceFiles) {
|
|
544
|
-
const dictionaries = new Map(sourceFiles.filter((sourceFile) => sourceFile.file.endsWith(".dictionary.ts")).map((sourceFile) => [path2.dirname(sourceFile.file), sourceFile]));
|
|
545
|
-
return sourceFiles.filter((sourceFile) => sourceFile.file.endsWith(".store.ts")).flatMap((sourceFile) => this.#scanStore(sourceFile, dictionaries.get(path2.dirname(sourceFile.file))));
|
|
546
|
-
}
|
|
547
|
-
#scanStore(store, dictionary) {
|
|
548
|
-
const actions = StoreScanner.#customActions(store);
|
|
549
|
-
if (!actions.length)
|
|
550
|
-
return [];
|
|
551
|
-
const described = dictionary ? StoreScanner.#describedEntries(dictionary) : new Map;
|
|
552
|
-
return actions.filter(({ name, fetched }) => fetched.length && !fetched.includes(name)).filter(({ name }) => !described.get("store")?.has(name) && !described.get("endpoint")?.has(name)).map(({ name, line, fetched }) => ({
|
|
553
|
-
rule: "akan.agent.missing-store-description",
|
|
554
|
-
scope: "agent",
|
|
555
|
-
severity: "warning",
|
|
556
|
-
file: store.file,
|
|
557
|
-
line,
|
|
558
|
-
message: `Store action "${name}" calls ${fetched.map((key) => `${key}()`).join(", ")} under a different name and has no dictionary .store() entry, so an agent reading it has the name and nothing else.`
|
|
559
|
-
}));
|
|
560
|
-
}
|
|
561
|
-
static #customActions(store) {
|
|
562
|
-
const actions = [];
|
|
563
|
-
const visit = (node) => {
|
|
564
|
-
if (ts3.isClassDeclaration(node) && StoreScanner.#extendsStore(node)) {
|
|
565
|
-
for (const member of node.members) {
|
|
566
|
-
if (!ts3.isMethodDeclaration(member))
|
|
567
|
-
continue;
|
|
568
|
-
if (member.modifiers?.some((modifier) => modifier.kind === ts3.SyntaxKind.StaticKeyword))
|
|
569
|
-
continue;
|
|
570
|
-
const name = StoreScanner.#memberName(member);
|
|
571
|
-
if (!name)
|
|
572
|
-
continue;
|
|
573
|
-
const line = store.sourceFile.getLineAndCharacterOfPosition(member.getStart(store.sourceFile)).line + 1;
|
|
574
|
-
actions.push({ name, line, fetched: StoreScanner.#fetchedEndpoints(member) });
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
ts3.forEachChild(node, visit);
|
|
578
|
-
};
|
|
579
|
-
visit(store.sourceFile);
|
|
580
|
-
return actions;
|
|
581
|
-
}
|
|
582
|
-
static #extendsStore(node) {
|
|
583
|
-
return !!node.heritageClauses?.some((clause) => clause.types.some((type) => ts3.isCallExpression(type.expression) && StoreScanner.#isStoreCall(type.expression)));
|
|
584
|
-
}
|
|
585
|
-
static #isStoreCall(expression) {
|
|
586
|
-
return ts3.isIdentifier(expression.expression) && expression.expression.text === "store";
|
|
587
|
-
}
|
|
588
|
-
static #fetchedEndpoints(member) {
|
|
589
|
-
const fetched = new Set;
|
|
590
|
-
const visit = (node) => {
|
|
591
|
-
if (ts3.isCallExpression(node) && ts3.isPropertyAccessExpression(node.expression) && ts3.isIdentifier(node.expression.expression) && node.expression.expression.text === "fetch")
|
|
592
|
-
fetched.add(node.expression.name.text);
|
|
593
|
-
ts3.forEachChild(node, visit);
|
|
594
|
-
};
|
|
595
|
-
visit(member);
|
|
596
|
-
return [...fetched];
|
|
597
|
-
}
|
|
598
|
-
static #describedEntries(dictionary) {
|
|
599
|
-
const described = new Map;
|
|
600
|
-
const visit = (node) => {
|
|
601
|
-
const stage = StoreScanner.#dictionaryStage(node);
|
|
602
|
-
if (stage) {
|
|
603
|
-
for (const [name, chain] of StoreScanner.#stageEntries(node)) {
|
|
604
|
-
if (!chain.has("desc"))
|
|
605
|
-
continue;
|
|
606
|
-
const names = described.get(stage) ?? new Set;
|
|
607
|
-
names.add(name);
|
|
608
|
-
described.set(stage, names);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
ts3.forEachChild(node, visit);
|
|
612
|
-
};
|
|
613
|
-
visit(dictionary.sourceFile);
|
|
614
|
-
return described;
|
|
615
|
-
}
|
|
616
|
-
static #dictionaryStage(node) {
|
|
617
|
-
if (!ts3.isCallExpression(node) || !ts3.isPropertyAccessExpression(node.expression))
|
|
618
|
-
return null;
|
|
619
|
-
const stage = node.expression.name.text;
|
|
620
|
-
return stage === "store" || stage === "endpoint" ? stage : null;
|
|
621
|
-
}
|
|
622
|
-
static #stageEntries(stage) {
|
|
623
|
-
const callback = stage.arguments[0];
|
|
624
|
-
if (!callback || !ts3.isArrowFunction(callback))
|
|
625
|
-
return [];
|
|
626
|
-
const body = ts3.isParenthesizedExpression(callback.body) ? callback.body.expression : callback.body;
|
|
627
|
-
if (!ts3.isObjectLiteralExpression(body))
|
|
628
|
-
return [];
|
|
629
|
-
return body.properties.flatMap((property) => {
|
|
630
|
-
if (!ts3.isPropertyAssignment(property))
|
|
631
|
-
return [];
|
|
632
|
-
const name = StoreScanner.#memberName(property);
|
|
633
|
-
return name ? [[name, StoreScanner.#chainCalls(property.initializer)]] : [];
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
static #chainCalls(expression) {
|
|
637
|
-
const calls = new Set;
|
|
638
|
-
let current = expression;
|
|
639
|
-
while (ts3.isCallExpression(current) || ts3.isPropertyAccessExpression(current)) {
|
|
640
|
-
if (ts3.isPropertyAccessExpression(current))
|
|
641
|
-
calls.add(current.name.text);
|
|
642
|
-
current = current.expression;
|
|
643
|
-
}
|
|
644
|
-
return calls;
|
|
645
|
-
}
|
|
646
|
-
static #memberName(member) {
|
|
647
|
-
const { name } = member;
|
|
648
|
-
if (!name || !ts3.isIdentifier(name) && !ts3.isStringLiteral(name))
|
|
649
|
-
return null;
|
|
650
|
-
return name.text;
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
456
|
// pkgs/@akanjs/devkit/qualityScanner.ts
|
|
655
457
|
var MAX_FILE_LINES = 2000;
|
|
656
458
|
var PLACEHOLDER_EXPORT_NAMES = new Set([
|
|
@@ -672,7 +474,8 @@ var SUGGESTED_RULES = [
|
|
|
672
474
|
"Keep apps/*/lib and libs/*/lib root files limited to generated support facets such as cnst.ts, db.ts, dict.ts, sig.ts, srv.ts, st.ts, useClient.ts, and useServer.ts.",
|
|
673
475
|
"Use domain module folders consistently: lib/<model> for database modules, lib/_<service> for service modules, and lib/__scalar/<scalar> for scalar modules.",
|
|
674
476
|
"Keep module UI filenames predictable: database modules use <Model>.Template/Unit/Util/View/Zone.tsx, service modules use <Service>.Util/Zone.tsx, and scalar modules use <Scalar>.Template/Unit.tsx.",
|
|
675
|
-
"
|
|
477
|
+
"Hand a form control its store setter by reference so the field publishes an agent tool; put normalization in the control's transform prop and a multi-write in a _postSet<Field> hook on the store.",
|
|
478
|
+
"Move shared app and lib utilities to common/ instead of creating apps/*/base or libs/*/base.",
|
|
676
479
|
"Avoid large mixed-purpose class files; class export files should import helpers from neighboring utility files instead of declaring them inline."
|
|
677
480
|
];
|
|
678
481
|
var CONVENTION_SUFFIXES = [
|
|
@@ -690,25 +493,27 @@ var RULE_FIXES = {
|
|
|
690
493
|
"akan.file.max-lines": "Break the file into smaller focused modules; keep one primary responsibility per file.",
|
|
691
494
|
"akan.file.abstract-max-lines": "Run `akan compact <app-or-lib>` to rewrite the abstract with the AI editor, keeping only the invariants and workflows the source files cannot show.",
|
|
692
495
|
"akan.file.placeholder-export": "Remove the placeholder export; generated indexes should only re-export real modules.",
|
|
496
|
+
"akan.file.bang-comment-in-client": "Rewrite the marker as `// FIXME:` or `// TODO:`. Keep the bang form for server, srvkit, and CLI files.",
|
|
693
497
|
"akan.file.dictionary-stale-text": "Replace the scaffold text with real localized copy for this dictionary entry.",
|
|
694
498
|
"akan.file.global-declaration": "Move the global declaration into an approved low-level integration file (e.g. webkit) and keep it isolated.",
|
|
695
499
|
"akan.file.window-augmentation": "Move the Window augmentation into an approved browser integration file and keep it isolated.",
|
|
696
500
|
"akan.file.prototype-mutation": "Avoid prototype mutation, or isolate it in an approved low-level integration file.",
|
|
697
501
|
"akan.file.class-export-global-declaration": "Move the helper to a sibling file and import it into the class module.",
|
|
502
|
+
"akan.agent.unpublished-form-setter": "Pass the setter by reference where the wrapper only forwards (onChange={st.do.setTitleOnTask}); move normalization into the control's own transform prop; move a multi-write into a _postSet<Field> hook on the store, keeping the generated setter on the control.",
|
|
698
503
|
"akan.file.component-internal-declaration": "Move the type or helper to a type/util file in ui/, webkit/, or common/ by purpose. If it is the component's props, declare it as `interface <Component>Props`.",
|
|
699
504
|
"akan.file.component-export": "Move the value or type to a util/constant/type file in ui/, webkit/, or common/ and import it. Adding `export` is not a valid fix \u2014 only PascalCase components and their `<Component>Props` interface belong here.",
|
|
700
505
|
"akan.layout.app-root-file": "Move the file into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit).",
|
|
701
|
-
"akan.layout.
|
|
506
|
+
"akan.layout.app-root-folder": "Move the folder's contents into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit) and delete it.",
|
|
507
|
+
"akan.layout.lib-root-file": "Move the file into a conventional lib folder (common, env, lib, page, private, public, srvkit, ui, or webkit).",
|
|
508
|
+
"akan.layout.lib-root-folder": "Move the folder's contents into a conventional lib folder (common, env, lib, page, private, public, srvkit, ui, or webkit) and delete it.",
|
|
509
|
+
"akan.layout.lib-facet-file": "Move the file into a domain module folder under lib/; keep the lib facet root limited to generated support facets.",
|
|
702
510
|
"akan.layout.module-ui-file": "Rename the file to an allowed module UI name, or move it to ui/ if it is not a module component.",
|
|
703
511
|
"akan.ssr.unnecessary-use-client": 'Delete the "use client" directive so the file renders on the server. If it exists only to wrap one client child, drop the wrapper and use the child directly.',
|
|
704
512
|
"akan.ssr.client-static-component": "Move the component to a server file \u2014 a <Model>.Unit.tsx / <Model>.View.tsx for a module, or a ui/ file with no directive \u2014 and reference it from the client file.",
|
|
705
513
|
"akan.ssr.client-static-markup": "Keep the interactive element in the client component and hoist the static subtree into a server component, then accept it as `children` or render it through a Unit/View reference.",
|
|
706
514
|
"akan.ssr.client-mount-load": "Load the data in the route with `fetch.initX(...)` / `fetch.viewX(...)` and pass the init/view object down as a prop; the client store hydrates from it and the effect goes away.",
|
|
707
515
|
"akan.ssr.module-missing-server-view": "Add a <Model>.Unit.tsx for list/card rendering and a <Model>.View.tsx for the detail surface, then have the Zone delegate to them.",
|
|
708
|
-
"akan.ssr.template-client-state": "Bind the field to the store instead: `value={xForm.field}` with `onChange={st.do.setFieldOnX}`."
|
|
709
|
-
"akan.mcp.missing-description": "Add `.desc([en, ko])` to this entry in the module's dictionary \u2014 for a slice, either on the slice entry or on the `<model>List<Slice>` endpoint it generates. Describe when to reach for it, not what it is named.",
|
|
710
|
-
"akan.agent.missing-store-description": "Add the action to the module dictionary's `.store()` stage with a `.desc([en, ko])` saying what it does for the user \u2014 not what the endpoint it calls does. That stage is optional everywhere else: an action named after its endpoint already reads as that endpoint's description.",
|
|
711
|
-
"akan.mcp.unguarded-exposure": "Name the guards in the same option object as `mcp`: `init({ guards: [SignedIn], mcp: { expose: true } })`. Write `guards: [Public]` if anonymous reads are the intent \u2014 the access is the same, but only one of the two is a decision. The `slice()` call's guards map reaches the root slice and base CRUD, never a named slice."
|
|
516
|
+
"akan.ssr.template-client-state": "Bind the field to the store instead: `value={xForm.field}` with `onChange={st.do.setFieldOnX}`."
|
|
712
517
|
};
|
|
713
518
|
function getRuleFix(rule) {
|
|
714
519
|
if (rule.startsWith("akan.convention"))
|
|
@@ -729,9 +534,8 @@ class AkanQualityScanner {
|
|
|
729
534
|
...sourceFiles.flatMap((sourceFile) => this.#scanConventionQuality(sourceFile)),
|
|
730
535
|
...sourceFiles.flatMap((sourceFile) => this.#scanLayoutQuality(sourceFile)),
|
|
731
536
|
...abstractFiles.flatMap((abstractFile) => this.#scanAbstractQuality(abstractFile)),
|
|
732
|
-
...
|
|
733
|
-
...
|
|
734
|
-
...new StoreScanner().scan(sourceFiles)
|
|
537
|
+
...new FormSetterScanner().scan(sourceFiles),
|
|
538
|
+
...ssr.warnings
|
|
735
539
|
];
|
|
736
540
|
return {
|
|
737
541
|
workspaceRoot,
|
|
@@ -745,7 +549,7 @@ class AkanQualityScanner {
|
|
|
745
549
|
const ignoreFilter = ignore().add(await this.#readGitIgnore(workspaceRoot));
|
|
746
550
|
const files = [];
|
|
747
551
|
for (const targetRoot of ["apps", "libs"]) {
|
|
748
|
-
const absoluteTargetRoot =
|
|
552
|
+
const absoluteTargetRoot = path.join(workspaceRoot, targetRoot);
|
|
749
553
|
if (!await isDirectory(absoluteTargetRoot))
|
|
750
554
|
continue;
|
|
751
555
|
await this.#walkTargetFiles(workspaceRoot, absoluteTargetRoot, ignoreFilter, files);
|
|
@@ -753,7 +557,7 @@ class AkanQualityScanner {
|
|
|
753
557
|
return files.sort();
|
|
754
558
|
}
|
|
755
559
|
async#readGitIgnore(workspaceRoot) {
|
|
756
|
-
const gitIgnorePath =
|
|
560
|
+
const gitIgnorePath = path.join(workspaceRoot, ".gitignore");
|
|
757
561
|
if (!await Bun.file(gitIgnorePath).exists())
|
|
758
562
|
return [];
|
|
759
563
|
return (await readFile(gitIgnorePath, "utf8")).split(/\r?\n/);
|
|
@@ -761,8 +565,8 @@ class AkanQualityScanner {
|
|
|
761
565
|
async#walkTargetFiles(workspaceRoot, currentPath, ignoreFilter, files) {
|
|
762
566
|
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
763
567
|
for (const entry of entries) {
|
|
764
|
-
const absolutePath =
|
|
765
|
-
const relativePath = toPosix(
|
|
568
|
+
const absolutePath = path.join(currentPath, entry.name);
|
|
569
|
+
const relativePath = toPosix(path.relative(workspaceRoot, absolutePath));
|
|
766
570
|
if (shouldSkipPath(relativePath, entry.isDirectory(), ignoreFilter))
|
|
767
571
|
continue;
|
|
768
572
|
if (entry.isDirectory()) {
|
|
@@ -777,17 +581,17 @@ class AkanQualityScanner {
|
|
|
777
581
|
}
|
|
778
582
|
}
|
|
779
583
|
async#readSourceFile(workspaceRoot, file) {
|
|
780
|
-
const absolutePath =
|
|
584
|
+
const absolutePath = path.join(workspaceRoot, file);
|
|
781
585
|
const content = await readFile(absolutePath, "utf8");
|
|
782
586
|
return {
|
|
783
587
|
file,
|
|
784
588
|
absolutePath,
|
|
785
589
|
content,
|
|
786
|
-
sourceFile:
|
|
590
|
+
sourceFile: ts3.createSourceFile(file, content, ts3.ScriptTarget.Latest, true, getScriptKind(file))
|
|
787
591
|
};
|
|
788
592
|
}
|
|
789
593
|
async#readTextFile(workspaceRoot, file) {
|
|
790
|
-
return { file, content: await readFile(
|
|
594
|
+
return { file, content: await readFile(path.join(workspaceRoot, file), "utf8") };
|
|
791
595
|
}
|
|
792
596
|
#scanGlobalQuality(sourceFiles) {
|
|
793
597
|
const exportedFunctionLikes = sourceFiles.flatMap((sourceFile) => getExportedFunctionLikes(sourceFile));
|
|
@@ -841,6 +645,7 @@ class AkanQualityScanner {
|
|
|
841
645
|
message: `File has ${lineCount} lines. Keep single files under ${MAX_FILE_LINES} lines.`
|
|
842
646
|
});
|
|
843
647
|
}
|
|
648
|
+
warnings.push(...getBangCommentWarnings(sourceFile));
|
|
844
649
|
warnings.push(...getPlaceholderExportWarnings(sourceFile));
|
|
845
650
|
warnings.push(...getDictionaryTextWarnings(sourceFile));
|
|
846
651
|
warnings.push(...getGlobalMutationWarnings(sourceFile));
|
|
@@ -911,7 +716,7 @@ class AkanQualityScanner {
|
|
|
911
716
|
const suffix = CONVENTION_SUFFIXES.find((candidate) => sourceFile.file.endsWith(candidate));
|
|
912
717
|
if (!suffix)
|
|
913
718
|
return [];
|
|
914
|
-
const modelName = toPascalCase(
|
|
719
|
+
const modelName = toPascalCase(path.basename(sourceFile.file, suffix));
|
|
915
720
|
const warnings = [];
|
|
916
721
|
for (const declaration of getTopLevelDeclarations(sourceFile)) {
|
|
917
722
|
if (isAllowedConventionDeclaration(suffix, modelName, declaration))
|
|
@@ -922,7 +727,7 @@ class AkanQualityScanner {
|
|
|
922
727
|
severity: "warning",
|
|
923
728
|
file: sourceFile.file,
|
|
924
729
|
line: declaration.line,
|
|
925
|
-
message: `${
|
|
730
|
+
message: `${path.basename(sourceFile.file)} should not declare top-level ${declaration.kind} "${declaration.name}". Allowed declarations: ${getConventionDescription(suffix, modelName)}.`
|
|
926
731
|
});
|
|
927
732
|
}
|
|
928
733
|
return warnings;
|
|
@@ -942,25 +747,30 @@ class AkanQualityScanner {
|
|
|
942
747
|
];
|
|
943
748
|
}
|
|
944
749
|
#scanLayoutQuality(sourceFile) {
|
|
945
|
-
const segments = sourceFile.file.split("/");
|
|
946
750
|
const warnings = [];
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
751
|
+
const rootEntry = getSysRootEntry(sourceFile.file);
|
|
752
|
+
if (rootEntry) {
|
|
753
|
+
const { type, name, isDir } = rootEntry;
|
|
754
|
+
const allowed = isDir ? rootAllowedDirs[type].has(name) : rootAllowedFiles[type].has(name);
|
|
755
|
+
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
|
+
}
|
|
955
765
|
}
|
|
956
|
-
const
|
|
957
|
-
if (
|
|
766
|
+
const libFacetFile = getLibFacetRootFile(sourceFile.file);
|
|
767
|
+
if (libFacetFile && !isAllowedLibFacetRootFile(libFacetFile)) {
|
|
958
768
|
warnings.push({
|
|
959
|
-
rule: "akan.layout.lib-
|
|
769
|
+
rule: "akan.layout.lib-facet-file",
|
|
960
770
|
scope: "layout",
|
|
961
771
|
severity: "warning",
|
|
962
772
|
file: sourceFile.file,
|
|
963
|
-
message: `Unexpected lib root file "${
|
|
773
|
+
message: `Unexpected lib facet root file "${libFacetFile}". Keep direct lib/ root files limited to generated support facets.`
|
|
964
774
|
});
|
|
965
775
|
}
|
|
966
776
|
const moduleUiWarning = getModuleUiWarning(sourceFile.file);
|
|
@@ -1030,7 +840,7 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
1030
840
|
const declarations = [];
|
|
1031
841
|
const nameExempt = isPageRouteFile(sourceFile.file) || isUiComponentFile(sourceFile.file);
|
|
1032
842
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
1033
|
-
if (
|
|
843
|
+
if (ts3.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
1034
844
|
declarations.push({
|
|
1035
845
|
name: statement.name.text,
|
|
1036
846
|
kind: "function",
|
|
@@ -1040,7 +850,7 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
1040
850
|
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false)
|
|
1041
851
|
});
|
|
1042
852
|
}
|
|
1043
|
-
if (
|
|
853
|
+
if (ts3.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
1044
854
|
declarations.push({
|
|
1045
855
|
name: statement.name.text,
|
|
1046
856
|
kind: "class",
|
|
@@ -1050,9 +860,9 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
1050
860
|
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClassStatement(sourceFile.sourceFile, statement))
|
|
1051
861
|
});
|
|
1052
862
|
}
|
|
1053
|
-
if (
|
|
863
|
+
if (ts3.isVariableStatement(statement) && isExported(statement)) {
|
|
1054
864
|
for (const declaration of statement.declarationList.declarations) {
|
|
1055
|
-
if (!
|
|
865
|
+
if (!ts3.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
|
|
1056
866
|
continue;
|
|
1057
867
|
declarations.push({
|
|
1058
868
|
name: declaration.name.text,
|
|
@@ -1091,14 +901,14 @@ function isInLibModule(file) {
|
|
|
1091
901
|
return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
|
|
1092
902
|
}
|
|
1093
903
|
function isEnumClassStatement(sourceFile, statement) {
|
|
1094
|
-
if (!
|
|
904
|
+
if (!ts3.isClassDeclaration(statement))
|
|
1095
905
|
return false;
|
|
1096
|
-
const heritageClause = statement.heritageClauses?.find((clause) => clause.token ===
|
|
906
|
+
const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts3.SyntaxKind.ExtendsKeyword);
|
|
1097
907
|
const expression = heritageClause?.types[0]?.expression;
|
|
1098
908
|
return !!expression && expression.getText(sourceFile).startsWith("enumOf(");
|
|
1099
909
|
}
|
|
1100
910
|
function getExportedClassNames(sourceFile) {
|
|
1101
|
-
return sourceFile.statements.filter((statement) =>
|
|
911
|
+
return sourceFile.statements.filter((statement) => ts3.isClassDeclaration(statement)).filter((statement) => isExported(statement)).map((statement) => statement.name?.text).filter((name) => !!name);
|
|
1102
912
|
}
|
|
1103
913
|
function isComponentDeclarationFile(file) {
|
|
1104
914
|
if (!file.endsWith(".tsx"))
|
|
@@ -1114,7 +924,7 @@ function isComponentDeclarationFile(file) {
|
|
|
1114
924
|
function getComponentFileDeclarations(sourceFile) {
|
|
1115
925
|
const reExportedNames = new Set;
|
|
1116
926
|
for (const statement of sourceFile.statements) {
|
|
1117
|
-
if (
|
|
927
|
+
if (ts3.isExportDeclaration(statement) && statement.exportClause && ts3.isNamedExports(statement.exportClause)) {
|
|
1118
928
|
for (const element of statement.exportClause.elements)
|
|
1119
929
|
reExportedNames.add((element.propertyName ?? element.name).text);
|
|
1120
930
|
}
|
|
@@ -1124,19 +934,19 @@ function getComponentFileDeclarations(sourceFile) {
|
|
|
1124
934
|
const isDefaultExport = isDefaultExportStatement(statement);
|
|
1125
935
|
const inlineExported = isExported(statement);
|
|
1126
936
|
const add = (name, kind, line) => declarations.push({ name, kind, line, exported: inlineExported || reExportedNames.has(name), isDefaultExport });
|
|
1127
|
-
if (
|
|
937
|
+
if (ts3.isInterfaceDeclaration(statement))
|
|
1128
938
|
add(statement.name.text, "interface", getLine(sourceFile, statement));
|
|
1129
|
-
else if (
|
|
939
|
+
else if (ts3.isTypeAliasDeclaration(statement))
|
|
1130
940
|
add(statement.name.text, "type", getLine(sourceFile, statement));
|
|
1131
|
-
else if (
|
|
941
|
+
else if (ts3.isEnumDeclaration(statement))
|
|
1132
942
|
add(statement.name.text, "enum", getLine(sourceFile, statement));
|
|
1133
|
-
else if (
|
|
943
|
+
else if (ts3.isFunctionDeclaration(statement) && statement.name)
|
|
1134
944
|
add(statement.name.text, "function", getLine(sourceFile, statement));
|
|
1135
|
-
else if (
|
|
945
|
+
else if (ts3.isClassDeclaration(statement) && statement.name)
|
|
1136
946
|
add(statement.name.text, "class", getLine(sourceFile, statement));
|
|
1137
|
-
else if (
|
|
947
|
+
else if (ts3.isVariableStatement(statement)) {
|
|
1138
948
|
for (const declaration of statement.declarationList.declarations) {
|
|
1139
|
-
if (!
|
|
949
|
+
if (!ts3.isIdentifier(declaration.name))
|
|
1140
950
|
continue;
|
|
1141
951
|
const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
|
|
1142
952
|
add(declaration.name.text, kind, getLine(sourceFile, declaration));
|
|
@@ -1148,15 +958,15 @@ function getComponentFileDeclarations(sourceFile) {
|
|
|
1148
958
|
function getCompoundComponentNames(sourceFile) {
|
|
1149
959
|
const names = new Set;
|
|
1150
960
|
for (const statement of sourceFile.statements) {
|
|
1151
|
-
if (!
|
|
961
|
+
if (!ts3.isExpressionStatement(statement))
|
|
1152
962
|
continue;
|
|
1153
963
|
const { expression } = statement;
|
|
1154
|
-
if (!
|
|
964
|
+
if (!ts3.isBinaryExpression(expression) || expression.operatorToken.kind !== ts3.SyntaxKind.EqualsToken)
|
|
1155
965
|
continue;
|
|
1156
|
-
if (!
|
|
966
|
+
if (!ts3.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
|
|
1157
967
|
continue;
|
|
1158
968
|
names.add(expression.left.name.text);
|
|
1159
|
-
if (
|
|
969
|
+
if (ts3.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
|
|
1160
970
|
names.add(expression.right.text);
|
|
1161
971
|
}
|
|
1162
972
|
return names;
|
|
@@ -1176,9 +986,37 @@ function isPascalCaseName(name) {
|
|
|
1176
986
|
return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
|
|
1177
987
|
}
|
|
1178
988
|
function isDefaultExportStatement(statement) {
|
|
1179
|
-
if (
|
|
989
|
+
if (ts3.isExportAssignment(statement))
|
|
1180
990
|
return !statement.isExportEquals;
|
|
1181
|
-
|
|
991
|
+
if (!ts3.canHaveModifiers(statement))
|
|
992
|
+
return false;
|
|
993
|
+
return !!ts3.getModifiers(statement)?.some((modifier) => modifier.kind === ts3.SyntaxKind.DefaultKeyword);
|
|
994
|
+
}
|
|
995
|
+
function getBangCommentWarnings(sourceFile) {
|
|
996
|
+
if (!isClientReachableFile(sourceFile.file))
|
|
997
|
+
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
|
+
}));
|
|
1006
|
+
}
|
|
1007
|
+
function isClientReachableFile(file) {
|
|
1008
|
+
if (/\.(test|spec)\.tsx?$/.test(file))
|
|
1009
|
+
return false;
|
|
1010
|
+
if (/\.(constant|store)\.ts$/.test(file))
|
|
1011
|
+
return true;
|
|
1012
|
+
if (/\.(Template|Unit|Util|View|Zone)\.tsx$/.test(file))
|
|
1013
|
+
return true;
|
|
1014
|
+
const segments = file.split("/");
|
|
1015
|
+
if (segments.includes("ui") || segments.includes("webkit"))
|
|
1016
|
+
return /\.tsx?$/.test(file);
|
|
1017
|
+
if (segments.includes("common"))
|
|
1018
|
+
return file.endsWith(".ts");
|
|
1019
|
+
return segments.includes("page") && file.endsWith(".tsx");
|
|
1182
1020
|
}
|
|
1183
1021
|
function getPlaceholderExportWarnings(sourceFile) {
|
|
1184
1022
|
if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx"))
|
|
@@ -1208,7 +1046,7 @@ function getDictionaryTextWarnings(sourceFile) {
|
|
|
1208
1046
|
function getGlobalMutationWarnings(sourceFile) {
|
|
1209
1047
|
const warnings = [];
|
|
1210
1048
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
1211
|
-
if (
|
|
1049
|
+
if (ts3.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global") {
|
|
1212
1050
|
warnings.push({
|
|
1213
1051
|
rule: "akan.file.global-declaration",
|
|
1214
1052
|
scope: "file",
|
|
@@ -1218,7 +1056,7 @@ function getGlobalMutationWarnings(sourceFile) {
|
|
|
1218
1056
|
message: "Global declarations require an explicit low-level integration allowlist."
|
|
1219
1057
|
});
|
|
1220
1058
|
}
|
|
1221
|
-
if (
|
|
1059
|
+
if (ts3.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
|
|
1222
1060
|
warnings.push({
|
|
1223
1061
|
rule: "akan.file.window-augmentation",
|
|
1224
1062
|
scope: "file",
|
|
@@ -1228,7 +1066,7 @@ function getGlobalMutationWarnings(sourceFile) {
|
|
|
1228
1066
|
message: "Window augmentation should be isolated to approved browser integration files."
|
|
1229
1067
|
});
|
|
1230
1068
|
}
|
|
1231
|
-
if (
|
|
1069
|
+
if (ts3.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype.")) {
|
|
1232
1070
|
warnings.push({
|
|
1233
1071
|
rule: "akan.file.prototype-mutation",
|
|
1234
1072
|
scope: "file",
|
|
@@ -1246,23 +1084,23 @@ function getTopLevelDeclarations(sourceFile) {
|
|
|
1246
1084
|
}
|
|
1247
1085
|
function getTopLevelDeclaration(sourceFile, statement) {
|
|
1248
1086
|
const line = getLine(sourceFile, statement);
|
|
1249
|
-
if (
|
|
1087
|
+
if (ts3.isClassDeclaration(statement) && statement.name) {
|
|
1250
1088
|
return [{ name: statement.name.text, kind: "class", line, exported: isExported(statement), node: statement }];
|
|
1251
1089
|
}
|
|
1252
|
-
if (
|
|
1090
|
+
if (ts3.isFunctionDeclaration(statement) && statement.name) {
|
|
1253
1091
|
return [{ name: statement.name.text, kind: "function", line, exported: isExported(statement), node: statement }];
|
|
1254
1092
|
}
|
|
1255
|
-
if (
|
|
1093
|
+
if (ts3.isInterfaceDeclaration(statement)) {
|
|
1256
1094
|
return [{ name: statement.name.text, kind: "interface", line, exported: isExported(statement), node: statement }];
|
|
1257
1095
|
}
|
|
1258
|
-
if (
|
|
1096
|
+
if (ts3.isTypeAliasDeclaration(statement)) {
|
|
1259
1097
|
return [{ name: statement.name.text, kind: "type", line, exported: isExported(statement), node: statement }];
|
|
1260
1098
|
}
|
|
1261
|
-
if (
|
|
1099
|
+
if (ts3.isEnumDeclaration(statement)) {
|
|
1262
1100
|
return [{ name: statement.name.text, kind: "enum", line, exported: isExported(statement), node: statement }];
|
|
1263
1101
|
}
|
|
1264
|
-
if (
|
|
1265
|
-
return statement.declarationList.declarations.filter((declaration) =>
|
|
1102
|
+
if (ts3.isVariableStatement(statement)) {
|
|
1103
|
+
return statement.declarationList.declarations.filter((declaration) => ts3.isIdentifier(declaration.name)).map((declaration) => ({
|
|
1266
1104
|
name: declaration.name.text,
|
|
1267
1105
|
kind: "variable",
|
|
1268
1106
|
line: getLine(sourceFile, declaration),
|
|
@@ -1270,7 +1108,7 @@ function getTopLevelDeclaration(sourceFile, statement) {
|
|
|
1270
1108
|
node: statement
|
|
1271
1109
|
}));
|
|
1272
1110
|
}
|
|
1273
|
-
if (
|
|
1111
|
+
if (ts3.isExportDeclaration(statement)) {
|
|
1274
1112
|
return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
|
|
1275
1113
|
}
|
|
1276
1114
|
return [];
|
|
@@ -1295,9 +1133,9 @@ function isAllowedConstantDeclaration(modelName, declaration) {
|
|
|
1295
1133
|
return false;
|
|
1296
1134
|
if ([`${modelName}Input`, `${modelName}Object`, modelName, `Light${modelName}`, `${modelName}Insight`].includes(declaration.name))
|
|
1297
1135
|
return true;
|
|
1298
|
-
if (!
|
|
1136
|
+
if (!ts3.isClassDeclaration(declaration.node))
|
|
1299
1137
|
return false;
|
|
1300
|
-
const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token ===
|
|
1138
|
+
const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token === ts3.SyntaxKind.ExtendsKeyword);
|
|
1301
1139
|
const expression = heritageClause?.types[0]?.expression;
|
|
1302
1140
|
return !!expression && expression.getText().startsWith("enumOf(");
|
|
1303
1141
|
}
|
|
@@ -1314,7 +1152,19 @@ function getConventionDescription(suffix, modelName) {
|
|
|
1314
1152
|
return `${modelName}Internal, ${modelName}Slice, or ${modelName}Endpoint`;
|
|
1315
1153
|
return `${modelName}Store`;
|
|
1316
1154
|
}
|
|
1317
|
-
|
|
1155
|
+
var sysRootTypes = { apps: "app", libs: "lib" };
|
|
1156
|
+
function getSysRootEntry(file) {
|
|
1157
|
+
const segments = file.split("/");
|
|
1158
|
+
const root = segments[0];
|
|
1159
|
+
const name = segments[2];
|
|
1160
|
+
if (!root || !name || segments.length < 3)
|
|
1161
|
+
return null;
|
|
1162
|
+
const type = sysRootTypes[root];
|
|
1163
|
+
if (!type)
|
|
1164
|
+
return null;
|
|
1165
|
+
return { type, name, isDir: segments.length > 3 };
|
|
1166
|
+
}
|
|
1167
|
+
function getLibFacetRootFile(file) {
|
|
1318
1168
|
const segments = file.split("/");
|
|
1319
1169
|
if ((segments[0] === "libs" || segments[0] === "apps") && segments.length === 4 && segments[2] === "lib")
|
|
1320
1170
|
return segments[3];
|
|
@@ -1363,13 +1213,13 @@ function getModuleInfo(file) {
|
|
|
1363
1213
|
return { moduleName, fileName, kind: "database" };
|
|
1364
1214
|
}
|
|
1365
1215
|
function isExportedConst(declaration) {
|
|
1366
|
-
return declaration.exported &&
|
|
1216
|
+
return declaration.exported && ts3.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts3.NodeFlags.Const) !== 0;
|
|
1367
1217
|
}
|
|
1368
1218
|
function isExported(node) {
|
|
1369
|
-
return !!
|
|
1219
|
+
return !!ts3.getCombinedModifierFlags(node) && !!(ts3.getCombinedModifierFlags(node) & ts3.ModifierFlags.Export);
|
|
1370
1220
|
}
|
|
1371
1221
|
function isFunctionLikeInitializer(node) {
|
|
1372
|
-
return !!node && (
|
|
1222
|
+
return !!node && (ts3.isArrowFunction(node) || ts3.isFunctionExpression(node));
|
|
1373
1223
|
}
|
|
1374
1224
|
function getBodyFingerprint(sourceFile, node) {
|
|
1375
1225
|
if (!node)
|
|
@@ -1391,7 +1241,7 @@ async function isDirectory(absolutePath) {
|
|
|
1391
1241
|
}
|
|
1392
1242
|
}
|
|
1393
1243
|
function getScriptKind(file) {
|
|
1394
|
-
return file.endsWith(".tsx") ?
|
|
1244
|
+
return file.endsWith(".tsx") ? ts3.ScriptKind.TSX : ts3.ScriptKind.TS;
|
|
1395
1245
|
}
|
|
1396
1246
|
function getLine(sourceFile, node) {
|
|
1397
1247
|
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
@@ -1412,7 +1262,7 @@ function toPascalCase(value) {
|
|
|
1412
1262
|
return value.replace(/(^|[-_./])([a-zA-Z0-9])/g, (_, __, char) => char.toUpperCase()).replace(/[-_./]/g, "");
|
|
1413
1263
|
}
|
|
1414
1264
|
function toPosix(value) {
|
|
1415
|
-
return value.split(
|
|
1265
|
+
return value.split(path.sep).join("/");
|
|
1416
1266
|
}
|
|
1417
1267
|
function groupBy(items, getKey) {
|
|
1418
1268
|
const grouped = new Map;
|