@akanjs/cli 3.0.0-beta.19 → 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-6cm97kay.js → CodeAgent-awv258wb.js} +2 -2
- package/{CodeTui-za4jsm41.js → CodeTui-1nqh35ez.js} +20 -22
- package/TunnelShare-scf4e4pd.js +9 -0
- package/{agent.command-e0wpbbyg.js → agent.command-1s9ag7sb.js} +8 -9
- package/akanCode.js +4 -4
- package/{application.command-8wyxqc8q.js → application.command-49hwy6va.js} +25 -56
- package/{applicationBuildRunner-n9gzaxd0.js → applicationBuildRunner-ybyp97pv.js} +33 -59
- package/{applicationReleasePackager-vpggfqgq.js → applicationReleasePackager-esnrphw8.js} +40 -60
- package/buildBatch.proc.js +19 -24
- package/{capacitorApp-q3z0qht0.js → capacitorApp-1synz4en.js} +3 -4
- package/{cloud.command-4zwa5dsx.js → cloud.command-d7rqtzt9.js} +16 -26
- package/cloud.script-bkbpcr96.js +19 -0
- package/{code.command-468c9d7s.js → code.command-nn48xzej.js} +22 -37
- package/context.command-s3mcmvpa.js +48 -0
- package/context.runner-k99nb5pk.js +21 -0
- package/{dependencyScanner-0z2h2tks.js → dependencyScanner-ye2e6pdz.js} +2 -2
- package/{devTui-ffjsarqa.js → devTui-q7k7bakd.js} +5 -9
- package/fileSys-5ngz93ga.js +9 -0
- package/{guideline.command-vhw13pqr.js → guideline.command-sct4qnt5.js} +4 -5
- package/guidelines/conventions/conventions.instruction.md +5 -1
- package/guidelines/mcpRule/mcpRule.instruction.md +7 -5
- package/incrementalBuilder.proc.js +30 -48
- package/{index-s5rhm04y.js → index-0bq14vr1.js} +1 -1
- package/{index-10dkrrww.js → index-13ffe9wp.js} +4 -4
- package/{index-srmay5jw.js → index-22d57v4c.js} +84 -67
- package/{index-53176s7j.js → index-31yr441d.js} +10 -17
- package/index-3hx1z96c.js +87 -0
- package/{index-4m929vvn.js → index-3sq321ak.js} +10 -18
- package/{index-bhtmj233.js → index-3t7g9e4r.js} +41 -78
- package/{index-q0y2jq9j.js → index-5b6tavv3.js} +6 -3
- package/{index-47pj3aee.js → index-5e0n4hqx.js} +11 -12
- package/{index-7ep8qtvp.js → index-5pc1qrbv.js} +5 -8
- package/{index-esk82rvk.js → index-8mna0947.js} +34 -71
- package/{index-sq2dzkzm.js → index-8sf7wmdr.js} +6 -6
- package/{index-crmeqnmf.js → index-9d9bgcx4.js} +283 -722
- package/{index-bwf965h6.js → index-ahemdr5n.js} +6 -6
- package/{index-ab0c38ea.js → index-as7xmyz8.js} +20 -20
- package/{index-bc88j1ry.js → index-axzjaqve.js} +94 -142
- package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
- package/{index-667w0yah.js → index-c6eggbee.js} +2 -5
- package/{index-a1w6934z.js → index-ddxvdd1p.js} +169 -305
- package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
- package/{index-22xtgmhn.js → index-dzf3r49a.js} +1 -1
- package/{index-89hy31kf.js → index-fgq75haa.js} +172 -287
- package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
- package/{index-hwkygn9s.js → index-khpfpcsd.js} +302 -33
- package/{index-vqx8bdxz.js → index-mxav6j3c.js} +78 -231
- package/{index-2446k7b4.js → index-myhc3c1j.js} +512 -588
- package/{index-3jrp517k.js → index-nv5976x3.js} +45 -128
- package/{index-zqgj2rt2.js → index-przybvy5.js} +69 -147
- package/{index-n3cv0tm4.js → index-smme4r47.js} +1 -1
- package/{index-qdgzsnhg.js → index-w2zdcw3k.js} +110 -231
- package/{index-14zrtgfa.js → index-wa5jbxj1.js} +6 -39
- package/{index-bftkz6cp.js → index-ww4z397j.js} +198 -555
- package/{index-kwaq3n9n.js → index-xqjwd384.js} +4 -7
- package/{index-txr7dj2g.js → index-xqpzbab5.js} +106 -240
- package/{index-e4v6vhxq.js → index-xsjh6nzh.js} +178 -165
- package/{index-18zybgkc.js → index-ygn77a5w.js} +47 -99
- package/{index-9d6nvkz2.js → index-ztp3e73t.js} +79 -35
- package/index.js +23 -24
- package/{library.command-cqr175dy.js → library.command-1gx04jgn.js} +4 -5
- package/{localRegistry.command-49z55qqz.js → localRegistry.command-zndxj1q6.js} +21 -42
- package/{module.command-9p8mm3b0.js → module.command-3waddk95.js} +6 -7
- package/{package.command-wxyz909t.js → package.command-zrc7br1y.js} +5 -6
- package/package.json +2 -2
- package/{page.command-gsrsfqqs.js → page.command-vpjgardh.js} +4 -5
- package/{primitive.command-ns1mptje.js → primitive.command-sz84tk4t.js} +9 -20
- package/{prompter-1f8d9pcc.js → prompter-ptaa3nk0.js} +2 -2
- package/{quality.command-e76k2q9k.js → quality.command-g869hafs.js} +129 -363
- package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
- package/{repair.command-pkrsm3cw.js → repair.command-chsnjvvv.js} +11 -35
- package/{scalar.command-0cpt3fc8.js → scalar.command-2v21newx.js} +5 -6
- package/skills/akan-code/SKILL.md +12 -4
- package/{subspace.command-fktpwp0g.js → subspace.command-ckcbwfp1.js} +39 -58
- package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +3 -3
- package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +3 -3
- package/templates/client.ts +7 -30
- 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/server.ts +0 -2
- package/{tunnel.command-d5jy9gk8.js → tunnel.command-kd2yr4fx.js} +5 -6
- 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-gv84eh1p.js → workspace.command-fqcztt5p.js} +72 -107
- package/TunnelShare-2j6s511d.js +0 -9
- package/cloud.script-fwx3vb34.js +0 -20
- package/context.command-dsnw0jby.js +0 -80
- package/context.runner-rs4rj15d.js +0 -22
- package/devTuiView-1q1mdgrv.js +0 -9
- package/getDirname-dr3jz1xg.js +0 -7
- package/index-6wwzs1bc.js +0 -7
- package/workflow.command-6tkcysmx.js +0 -105
|
@@ -1,17 +1,15 @@
|
|
|
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";
|
|
12
|
+
} from "./index-9d9bgcx4.js";
|
|
15
13
|
|
|
16
14
|
// pkgs/@akanjs/devkit/qualityScanner.ts
|
|
17
15
|
import { createHash } from "crypto";
|
|
@@ -160,25 +158,11 @@ var STATIC_COMPONENT_MIN_MASS = 4;
|
|
|
160
158
|
var MIXED_COMPONENT_MIN_MASS = 10;
|
|
161
159
|
var MIXED_COMPONENT_MAX_TOUCHES = 2;
|
|
162
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 };
|
|
163
162
|
|
|
164
163
|
class SsrScanner {
|
|
165
164
|
static #serverSafeCalls = new Set(["usePage", "getSelf", "useServer"]);
|
|
166
|
-
static #clientGlobals = new Set(
|
|
167
|
-
"window",
|
|
168
|
-
"document",
|
|
169
|
-
"navigator",
|
|
170
|
-
"localStorage",
|
|
171
|
-
"sessionStorage",
|
|
172
|
-
"location",
|
|
173
|
-
"history",
|
|
174
|
-
"screen",
|
|
175
|
-
"matchMedia",
|
|
176
|
-
"IntersectionObserver",
|
|
177
|
-
"ResizeObserver",
|
|
178
|
-
"MutationObserver",
|
|
179
|
-
"requestAnimationFrame",
|
|
180
|
-
"WebSocket"
|
|
181
|
-
]);
|
|
165
|
+
static #clientGlobals = new Set("window document navigator localStorage sessionStorage location history screen matchMedia IntersectionObserver".split(" ").concat("ResizeObserver MutationObserver requestAnimationFrame WebSocket".split(" ")));
|
|
182
166
|
static #clientRuntimeImports = new Set(["st", "fetch"]);
|
|
183
167
|
scan(sourceFiles) {
|
|
184
168
|
const componentFiles = sourceFiles.filter((sourceFile) => this.#isBalancedFile(sourceFile.file));
|
|
@@ -199,38 +183,17 @@ class SsrScanner {
|
|
|
199
183
|
const components = this.#getComponents(sourceFile, vendorNames);
|
|
200
184
|
const warnings = [];
|
|
201
185
|
if (!hasVendorImport && !importsClientRuntime && this.#getTouches(sourceFile.sourceFile, sourceFile.sourceFile).length === 0 && !this.#isConventionClientFile(sourceFile.file)) {
|
|
202
|
-
warnings.push(
|
|
203
|
-
rule: "akan.ssr.unnecessary-use-client",
|
|
204
|
-
scope: "ssr",
|
|
205
|
-
severity: "warning",
|
|
206
|
-
file: sourceFile.file,
|
|
207
|
-
line: 1,
|
|
208
|
-
message: `"use client" is declared but the file uses no client-only capability (hook, event handler, store, or browser API).`
|
|
209
|
-
});
|
|
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));
|
|
210
187
|
}
|
|
211
188
|
for (const component of components) {
|
|
212
189
|
if (component.vendorTags)
|
|
213
190
|
continue;
|
|
214
191
|
if (component.touches.length === 0 && component.mass >= STATIC_COMPONENT_MIN_MASS) {
|
|
215
|
-
warnings.push({
|
|
216
|
-
rule: "akan.ssr.client-static-component",
|
|
217
|
-
scope: "ssr",
|
|
218
|
-
severity: "warning",
|
|
219
|
-
file: sourceFile.file,
|
|
220
|
-
line: component.line,
|
|
221
|
-
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.`
|
|
222
|
-
});
|
|
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));
|
|
223
193
|
continue;
|
|
224
194
|
}
|
|
225
195
|
if (component.touches.length >= 1 && component.touches.length <= MIXED_COMPONENT_MAX_TOUCHES && component.mass >= MIXED_COMPONENT_MIN_MASS) {
|
|
226
|
-
warnings.push({
|
|
227
|
-
rule: "akan.ssr.client-static-markup",
|
|
228
|
-
scope: "ssr",
|
|
229
|
-
severity: "warning",
|
|
230
|
-
file: sourceFile.file,
|
|
231
|
-
line: component.line,
|
|
232
|
-
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.`
|
|
233
|
-
});
|
|
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));
|
|
234
197
|
}
|
|
235
198
|
}
|
|
236
199
|
warnings.push(...this.#getMountLoadWarnings(sourceFile));
|
|
@@ -250,32 +213,16 @@ class SsrScanner {
|
|
|
250
213
|
entry.serverFiles += 1;
|
|
251
214
|
modules.set(moduleDir, entry);
|
|
252
215
|
}
|
|
253
|
-
return [...modules].filter(([, entry]) => entry.serverFiles === 0 && entry.clientMass >= MODULE_SERVER_VIEW_MIN_CLIENT_MASS).map(([moduleDir, entry]) => ({
|
|
254
|
-
rule: "akan.ssr.module-missing-server-view",
|
|
255
|
-
scope: "ssr",
|
|
256
|
-
severity: "warning",
|
|
257
|
-
file: entry.line,
|
|
258
|
-
message: `Module "${moduleDir}" renders ${entry.clientMass} JSX elements from client files only; it declares no Unit or View server component.`
|
|
259
|
-
}));
|
|
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.`));
|
|
260
217
|
}
|
|
261
218
|
#getMountLoadWarnings(sourceFile) {
|
|
262
219
|
const warnings = [];
|
|
263
|
-
|
|
264
|
-
if (this.#isMountEffect(sourceFile.sourceFile, node))
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
severity: "warning",
|
|
270
|
-
file: sourceFile.file,
|
|
271
|
-
line: this.#getLine(sourceFile.sourceFile, load.node),
|
|
272
|
-
message: `Mount-only effect loads server data with ${load.callee}(). The route can fetch this before the first byte instead.`
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
ts2.forEachChild(node, visit);
|
|
277
|
-
};
|
|
278
|
-
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
|
+
});
|
|
279
226
|
return warnings;
|
|
280
227
|
}
|
|
281
228
|
#isMountEffect(sourceFile, node) {
|
|
@@ -289,35 +236,23 @@ class SsrScanner {
|
|
|
289
236
|
}
|
|
290
237
|
#getLoadCalls(sourceFile, node) {
|
|
291
238
|
const calls = [];
|
|
292
|
-
|
|
293
|
-
if (ts2.isCallExpression(child))
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
};
|
|
300
|
-
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
|
+
});
|
|
301
246
|
return calls;
|
|
302
247
|
}
|
|
303
248
|
#getTemplateStateWarnings(sourceFile) {
|
|
304
249
|
if (!sourceFile.file.endsWith(".Template.tsx"))
|
|
305
250
|
return [];
|
|
306
251
|
const warnings = [];
|
|
307
|
-
|
|
308
|
-
if (ts2.isCallExpression(node) && node.expression.getText(sourceFile.sourceFile) === "useState")
|
|
309
|
-
warnings.push(
|
|
310
|
-
|
|
311
|
-
scope: "ssr",
|
|
312
|
-
severity: "warning",
|
|
313
|
-
file: sourceFile.file,
|
|
314
|
-
line: this.#getLine(sourceFile.sourceFile, node),
|
|
315
|
-
message: "Template holds form state in useState. Templates are store-driven and carry no local state."
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
ts2.forEachChild(node, visit);
|
|
319
|
-
};
|
|
320
|
-
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
|
+
});
|
|
321
256
|
return warnings;
|
|
322
257
|
}
|
|
323
258
|
#measureBalance(sourceFiles) {
|
|
@@ -372,7 +307,7 @@ class SsrScanner {
|
|
|
372
307
|
}
|
|
373
308
|
#getTouches(sourceFile, node) {
|
|
374
309
|
const touches = [];
|
|
375
|
-
|
|
310
|
+
SsrScanner.#walk(node, (child) => {
|
|
376
311
|
if (ts2.isCallExpression(child)) {
|
|
377
312
|
const callee = child.expression.getText(sourceFile);
|
|
378
313
|
const bareName = callee.split(".").pop() ?? callee;
|
|
@@ -390,31 +325,32 @@ class SsrScanner {
|
|
|
390
325
|
else if (SsrScanner.#clientGlobals.has(root))
|
|
391
326
|
touches.push(root);
|
|
392
327
|
}
|
|
393
|
-
|
|
394
|
-
};
|
|
395
|
-
ts2.forEachChild(node, visit);
|
|
328
|
+
});
|
|
396
329
|
return touches;
|
|
397
330
|
}
|
|
398
331
|
#getTagNames(sourceFile, node) {
|
|
399
332
|
const tags = new Set;
|
|
400
|
-
|
|
333
|
+
SsrScanner.#walk(node, (child) => {
|
|
401
334
|
if (ts2.isJsxOpeningElement(child) || ts2.isJsxSelfClosingElement(child))
|
|
402
335
|
tags.add(child.tagName.getText(sourceFile).split(".")[0]);
|
|
403
|
-
|
|
404
|
-
};
|
|
405
|
-
ts2.forEachChild(node, visit);
|
|
336
|
+
});
|
|
406
337
|
return tags;
|
|
407
338
|
}
|
|
408
339
|
#getMass(node) {
|
|
409
340
|
let mass = 0;
|
|
410
|
-
|
|
341
|
+
SsrScanner.#walk(node, (child) => {
|
|
411
342
|
if (ts2.isJsxOpeningElement(child) || ts2.isJsxSelfClosingElement(child))
|
|
412
343
|
mass += 1;
|
|
413
|
-
|
|
414
|
-
};
|
|
415
|
-
ts2.forEachChild(node, visit);
|
|
344
|
+
});
|
|
416
345
|
return mass;
|
|
417
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
|
+
}
|
|
418
354
|
#hasUseClient(sourceFile) {
|
|
419
355
|
const first = sourceFile.statements[0];
|
|
420
356
|
if (!first || !ts2.isExpressionStatement(first) || !ts2.isStringLiteral(first.expression))
|
|
@@ -688,24 +624,10 @@ class AkanQualityScanner {
|
|
|
688
624
|
const warnings = [];
|
|
689
625
|
const lineCount = sourceFile.content.split(/\r?\n/).length;
|
|
690
626
|
const recommendedLineLimit = getRecommendedLineLimit(sourceFile.file);
|
|
691
|
-
if (recommendedLineLimit && lineCount > recommendedLineLimit)
|
|
692
|
-
warnings.push({
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
severity: "warning",
|
|
696
|
-
file: sourceFile.file,
|
|
697
|
-
message: `File has ${lineCount} lines. Recommended limit for this file type is ${recommendedLineLimit} lines.`
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
if (lineCount > MAX_FILE_LINES) {
|
|
701
|
-
warnings.push({
|
|
702
|
-
rule: "akan.file.max-lines",
|
|
703
|
-
scope: "file",
|
|
704
|
-
severity: "warning",
|
|
705
|
-
file: sourceFile.file,
|
|
706
|
-
message: `File has ${lineCount} lines. Keep single files under ${MAX_FILE_LINES} lines.`
|
|
707
|
-
});
|
|
708
|
-
}
|
|
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.`));
|
|
709
631
|
warnings.push(...getBangCommentWarnings(sourceFile));
|
|
710
632
|
warnings.push(...getPlaceholderExportWarnings(sourceFile));
|
|
711
633
|
warnings.push(...getDictionaryTextWarnings(sourceFile));
|
|
@@ -719,14 +641,7 @@ class AkanQualityScanner {
|
|
|
719
641
|
continue;
|
|
720
642
|
if (declaration.kind === "interface" && allowedInterfaceNames.has(declaration.name))
|
|
721
643
|
continue;
|
|
722
|
-
warnings.push({
|
|
723
|
-
rule: "akan.file.class-export-global-declaration",
|
|
724
|
-
scope: "file",
|
|
725
|
-
severity: "warning",
|
|
726
|
-
file: sourceFile.file,
|
|
727
|
-
line: declaration.line,
|
|
728
|
-
message: `Class export files should not declare top-level ${declaration.kind} "${declaration.name}". Move helpers to another file and import them.`
|
|
729
|
-
});
|
|
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));
|
|
730
645
|
}
|
|
731
646
|
return warnings;
|
|
732
647
|
}
|
|
@@ -748,28 +663,13 @@ class AkanQualityScanner {
|
|
|
748
663
|
if (declaration.exported) {
|
|
749
664
|
if (isAllowedComponentExport(declaration, isPage))
|
|
750
665
|
continue;
|
|
751
|
-
warnings.push({
|
|
752
|
-
rule: "akan.file.component-export",
|
|
753
|
-
scope: "file",
|
|
754
|
-
severity: "warning",
|
|
755
|
-
file: sourceFile.file,
|
|
756
|
-
line: declaration.line,
|
|
757
|
-
message: `Component file exports ${declaration.kind} "${declaration.name}", which is not a PascalCase component${isPage ? " or reserved route export" : ""}.`
|
|
758
|
-
});
|
|
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));
|
|
759
667
|
continue;
|
|
760
668
|
}
|
|
761
669
|
if (isComponentValueKind(declaration.kind) && compoundComponentNames.has(declaration.name))
|
|
762
670
|
continue;
|
|
763
|
-
if (isRestrictedInternalKind(declaration.kind))
|
|
764
|
-
warnings.push({
|
|
765
|
-
rule: "akan.file.component-internal-declaration",
|
|
766
|
-
scope: "file",
|
|
767
|
-
severity: "warning",
|
|
768
|
-
file: sourceFile.file,
|
|
769
|
-
line: declaration.line,
|
|
770
|
-
message: `Component file declares non-exported ${declaration.kind} "${declaration.name}". Only "interface <Component>Props" may stay internal.`
|
|
771
|
-
});
|
|
772
|
-
}
|
|
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));
|
|
773
673
|
}
|
|
774
674
|
return warnings;
|
|
775
675
|
}
|
|
@@ -778,33 +678,14 @@ class AkanQualityScanner {
|
|
|
778
678
|
if (!suffix)
|
|
779
679
|
return [];
|
|
780
680
|
const modelName = toPascalCase(path2.basename(sourceFile.file, suffix));
|
|
781
|
-
|
|
782
|
-
for (const declaration of getTopLevelDeclarations(sourceFile)) {
|
|
783
|
-
if (isAllowedConventionDeclaration(suffix, modelName, declaration))
|
|
784
|
-
continue;
|
|
785
|
-
warnings.push({
|
|
786
|
-
rule: `akan.convention${suffix.replace(".ts", "")}`,
|
|
787
|
-
scope: "convention",
|
|
788
|
-
severity: "warning",
|
|
789
|
-
file: sourceFile.file,
|
|
790
|
-
line: declaration.line,
|
|
791
|
-
message: `${path2.basename(sourceFile.file)} should not declare top-level ${declaration.kind} "${declaration.name}". Allowed declarations: ${getConventionDescription(suffix, modelName)}.`
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
return warnings;
|
|
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));
|
|
795
682
|
}
|
|
796
683
|
#scanAbstractQuality({ file, content }) {
|
|
797
684
|
const lineCount = AbstractDoc.lineCountOf(content);
|
|
798
685
|
if (lineCount <= AbstractDoc.maxLines)
|
|
799
686
|
return [];
|
|
800
687
|
return [
|
|
801
|
-
{
|
|
802
|
-
rule: "akan.file.abstract-max-lines",
|
|
803
|
-
scope: "file",
|
|
804
|
-
severity: "warning",
|
|
805
|
-
file,
|
|
806
|
-
message: `Abstract has ${lineCount} lines. Keep abstracts under ${AbstractDoc.maxLines} lines and compact them periodically.`
|
|
807
|
-
}
|
|
688
|
+
fileWarning("akan.file.abstract-max-lines", "file", file, `Abstract has ${lineCount} lines. Keep abstracts under ${AbstractDoc.maxLines} lines and compact them periodically.`)
|
|
808
689
|
];
|
|
809
690
|
}
|
|
810
691
|
#scanLayoutQuality(sourceFile) {
|
|
@@ -814,26 +695,12 @@ class AkanQualityScanner {
|
|
|
814
695
|
const { type, name, isDir } = rootEntry;
|
|
815
696
|
const allowed = isDir ? rootAllowedDirs[type].has(name) : rootAllowedFiles[type].has(name);
|
|
816
697
|
const kind = isDir ? "folder" : "file";
|
|
817
|
-
if (!allowed)
|
|
818
|
-
warnings.push({
|
|
819
|
-
rule: `akan.layout.${type}-root-${kind}`,
|
|
820
|
-
scope: "layout",
|
|
821
|
-
severity: "warning",
|
|
822
|
-
file: sourceFile.file,
|
|
823
|
-
message: `Unexpected ${type} root ${kind} "${name}". Keep ${type} code in conventional ${type} folders.`
|
|
824
|
-
});
|
|
825
|
-
}
|
|
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.`));
|
|
826
700
|
}
|
|
827
701
|
const libFacetFile = getLibFacetRootFile(sourceFile.file);
|
|
828
|
-
if (libFacetFile && !isAllowedLibFacetRootFile(libFacetFile))
|
|
829
|
-
warnings.push({
|
|
830
|
-
rule: "akan.layout.lib-facet-file",
|
|
831
|
-
scope: "layout",
|
|
832
|
-
severity: "warning",
|
|
833
|
-
file: sourceFile.file,
|
|
834
|
-
message: `Unexpected lib facet root file "${libFacetFile}". Keep direct lib/ root files limited to generated support facets.`
|
|
835
|
-
});
|
|
836
|
-
}
|
|
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.`));
|
|
837
704
|
const moduleUiWarning = getModuleUiWarning(sourceFile.file);
|
|
838
705
|
if (moduleUiWarning)
|
|
839
706
|
warnings.push(moduleUiWarning);
|
|
@@ -900,67 +767,48 @@ function formatQualityLocation(file, line) {
|
|
|
900
767
|
function getExportedFunctionLikes(sourceFile) {
|
|
901
768
|
const declarations = [];
|
|
902
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
|
+
});
|
|
903
778
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
904
|
-
if (ts3.isFunctionDeclaration(statement) && statement.name && isExported(statement))
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
file: sourceFile.file,
|
|
909
|
-
line: getLine(sourceFile.sourceFile, statement),
|
|
910
|
-
bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, statement.body),
|
|
911
|
-
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false)
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
if (ts3.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
915
|
-
declarations.push({
|
|
916
|
-
name: statement.name.text,
|
|
917
|
-
kind: "class",
|
|
918
|
-
file: sourceFile.file,
|
|
919
|
-
line: getLine(sourceFile.sourceFile, statement),
|
|
920
|
-
bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, statement),
|
|
921
|
-
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClassStatement(sourceFile.sourceFile, statement))
|
|
922
|
-
});
|
|
923
|
-
}
|
|
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));
|
|
924
783
|
if (ts3.isVariableStatement(statement) && isExported(statement)) {
|
|
925
784
|
for (const declaration of statement.declarationList.declarations) {
|
|
926
785
|
if (!ts3.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
|
|
927
786
|
continue;
|
|
928
|
-
|
|
929
|
-
name: declaration.name.text,
|
|
930
|
-
kind: "function-variable",
|
|
931
|
-
file: sourceFile.file,
|
|
932
|
-
line: getLine(sourceFile.sourceFile, declaration),
|
|
933
|
-
bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, declaration.initializer),
|
|
934
|
-
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false)
|
|
935
|
-
});
|
|
787
|
+
add(declaration.name.text, "function-variable", declaration, declaration.initializer);
|
|
936
788
|
}
|
|
937
789
|
}
|
|
938
790
|
}
|
|
939
791
|
return declarations;
|
|
940
792
|
}
|
|
941
|
-
function
|
|
793
|
+
function sysSegments(file) {
|
|
942
794
|
const segments = file.split("/");
|
|
943
|
-
return
|
|
795
|
+
return segments[0] === "apps" || segments[0] === "libs" ? segments : null;
|
|
796
|
+
}
|
|
797
|
+
function isPageRouteFile(file) {
|
|
798
|
+
return sysSegments(file)?.[2] === "page";
|
|
944
799
|
}
|
|
945
800
|
function isUiComponentFile(file) {
|
|
946
|
-
|
|
947
|
-
return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "ui";
|
|
801
|
+
return sysSegments(file)?.[2] === "ui";
|
|
948
802
|
}
|
|
949
803
|
function isConventionDuplicateNameExempt(file, isEnumClass) {
|
|
950
|
-
if (!
|
|
804
|
+
if (!sysSegments(file)?.includes("lib"))
|
|
951
805
|
return false;
|
|
952
|
-
if (file.endsWith(".tsx"))
|
|
953
|
-
return true;
|
|
954
|
-
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))
|
|
955
807
|
return true;
|
|
956
808
|
if (file.endsWith(".constant.ts"))
|
|
957
809
|
return !isEnumClass;
|
|
958
810
|
return false;
|
|
959
811
|
}
|
|
960
|
-
function isInLibModule(file) {
|
|
961
|
-
const segments = file.split("/");
|
|
962
|
-
return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
|
|
963
|
-
}
|
|
964
812
|
function isEnumClassStatement(sourceFile, statement) {
|
|
965
813
|
if (!ts3.isClassDeclaration(statement))
|
|
966
814
|
return false;
|
|
@@ -1056,14 +904,7 @@ function isDefaultExportStatement(statement) {
|
|
|
1056
904
|
function getBangCommentWarnings(sourceFile) {
|
|
1057
905
|
if (!isClientReachableFile(sourceFile.file))
|
|
1058
906
|
return [];
|
|
1059
|
-
return findPatternLines(sourceFile.content, /(?:^|[^\s/][ \t]+)\/[*/]!/).map((line) => (
|
|
1060
|
-
rule: "akan.file.bang-comment-in-client",
|
|
1061
|
-
scope: "file",
|
|
1062
|
-
severity: "warning",
|
|
1063
|
-
file: sourceFile.file,
|
|
1064
|
-
line,
|
|
1065
|
-
message: "A `//!` or `/*!` marker survives minification and ships to the browser."
|
|
1066
|
-
}));
|
|
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));
|
|
1067
908
|
}
|
|
1068
909
|
function isClientReachableFile(file) {
|
|
1069
910
|
if (/\.(test|spec)\.tsx?$/.test(file))
|
|
@@ -1082,61 +923,24 @@ function isClientReachableFile(file) {
|
|
|
1082
923
|
function getPlaceholderExportWarnings(sourceFile) {
|
|
1083
924
|
if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx"))
|
|
1084
925
|
return [];
|
|
1085
|
-
return getTopLevelDeclarations(sourceFile).filter((declaration) => declaration.exported && PLACEHOLDER_EXPORT_NAMES.has(declaration.name)).map((declaration) => ({
|
|
1086
|
-
rule: "akan.file.placeholder-export",
|
|
1087
|
-
scope: "file",
|
|
1088
|
-
severity: "warning",
|
|
1089
|
-
file: sourceFile.file,
|
|
1090
|
-
line: declaration.line,
|
|
1091
|
-
message: `Generated or barrel index file should not export placeholder "${declaration.name}".`
|
|
1092
|
-
}));
|
|
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));
|
|
1093
927
|
}
|
|
1094
928
|
function getDictionaryTextWarnings(sourceFile) {
|
|
1095
929
|
if (!sourceFile.file.endsWith(".dictionary.ts"))
|
|
1096
930
|
return [];
|
|
1097
931
|
const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
|
|
1098
|
-
return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile.content, pattern).map((line) => ({
|
|
1099
|
-
rule: "akan.file.dictionary-stale-text",
|
|
1100
|
-
scope: "file",
|
|
1101
|
-
severity: "warning",
|
|
1102
|
-
file: sourceFile.file,
|
|
1103
|
-
line,
|
|
1104
|
-
message: `Dictionary text appears to contain ${label}.`
|
|
1105
|
-
})));
|
|
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)));
|
|
1106
933
|
}
|
|
1107
934
|
function getGlobalMutationWarnings(sourceFile) {
|
|
1108
935
|
const warnings = [];
|
|
1109
936
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
message: "Global declarations require an explicit low-level integration allowlist."
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1120
|
-
if (ts3.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
|
|
1121
|
-
warnings.push({
|
|
1122
|
-
rule: "akan.file.window-augmentation",
|
|
1123
|
-
scope: "file",
|
|
1124
|
-
severity: "warning",
|
|
1125
|
-
file: sourceFile.file,
|
|
1126
|
-
line: getLine(sourceFile.sourceFile, statement),
|
|
1127
|
-
message: "Window augmentation should be isolated to approved browser integration files."
|
|
1128
|
-
});
|
|
1129
|
-
}
|
|
1130
|
-
if (ts3.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype.")) {
|
|
1131
|
-
warnings.push({
|
|
1132
|
-
rule: "akan.file.prototype-mutation",
|
|
1133
|
-
scope: "file",
|
|
1134
|
-
severity: "warning",
|
|
1135
|
-
file: sourceFile.file,
|
|
1136
|
-
line: getLine(sourceFile.sourceFile, statement),
|
|
1137
|
-
message: "Prototype mutation should be avoided or isolated to approved low-level integration files."
|
|
1138
|
-
});
|
|
1139
|
-
}
|
|
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.");
|
|
1140
944
|
}
|
|
1141
945
|
return warnings;
|
|
1142
946
|
}
|
|
@@ -1145,73 +949,59 @@ function getTopLevelDeclarations(sourceFile) {
|
|
|
1145
949
|
}
|
|
1146
950
|
function getTopLevelDeclaration(sourceFile, statement) {
|
|
1147
951
|
const line = getLine(sourceFile, statement);
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
if (ts3.
|
|
1158
|
-
return [
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
if (ts3.
|
|
1164
|
-
return statement.
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
exported: isExported(statement),
|
|
1169
|
-
node: statement
|
|
1170
|
-
}));
|
|
1171
|
-
}
|
|
1172
|
-
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))
|
|
1173
972
|
return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
|
|
1174
|
-
}
|
|
1175
973
|
return [];
|
|
1176
974
|
}
|
|
1177
|
-
function
|
|
1178
|
-
if (suffix === ".dictionary.ts")
|
|
1179
|
-
return isExportedConst(declaration) && declaration.name === "dictionary";
|
|
975
|
+
function getConventionClassNames(suffix, model) {
|
|
1180
976
|
if (suffix === ".constant.ts")
|
|
1181
|
-
return
|
|
977
|
+
return [`${model}Input`, `${model}Object`, model, `Light${model}`, `${model}Insight`];
|
|
1182
978
|
if (suffix === ".document.ts")
|
|
1183
|
-
return
|
|
979
|
+
return [`${model}Filter`, model, `${model}Model`];
|
|
1184
980
|
if (suffix === ".service.ts")
|
|
1185
|
-
return
|
|
981
|
+
return [`${model}Service`];
|
|
1186
982
|
if (suffix === ".signal.ts")
|
|
1187
|
-
return
|
|
983
|
+
return [`${model}Internal`, `${model}Slice`, `${model}Endpoint`];
|
|
1188
984
|
if (suffix === ".store.ts")
|
|
1189
|
-
return
|
|
1190
|
-
return
|
|
985
|
+
return [`${model}Store`];
|
|
986
|
+
return [];
|
|
1191
987
|
}
|
|
1192
|
-
function
|
|
988
|
+
function isAllowedConventionDeclaration(suffix, modelName, declaration) {
|
|
989
|
+
if (suffix === ".dictionary.ts")
|
|
990
|
+
return isExportedConst(declaration) && declaration.name === "dictionary";
|
|
1193
991
|
if (declaration.kind !== "class")
|
|
1194
992
|
return false;
|
|
1195
|
-
if (
|
|
993
|
+
if (getConventionClassNames(suffix, modelName).includes(declaration.name))
|
|
1196
994
|
return true;
|
|
1197
|
-
|
|
1198
|
-
return false;
|
|
1199
|
-
const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token === ts3.SyntaxKind.ExtendsKeyword);
|
|
1200
|
-
const expression = heritageClause?.types[0]?.expression;
|
|
1201
|
-
return !!expression && expression.getText().startsWith("enumOf(");
|
|
995
|
+
return suffix === ".constant.ts" && isEnumClassStatement(declaration.node.getSourceFile(), declaration.node);
|
|
1202
996
|
}
|
|
1203
997
|
function getConventionDescription(suffix, modelName) {
|
|
1204
998
|
if (suffix === ".dictionary.ts")
|
|
1205
999
|
return "export const dictionary";
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
return `${modelName}Service`;
|
|
1212
|
-
if (suffix === ".signal.ts")
|
|
1213
|
-
return `${modelName}Internal, ${modelName}Slice, or ${modelName}Endpoint`;
|
|
1214
|
-
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];
|
|
1215
1005
|
}
|
|
1216
1006
|
var sysRootTypes = { apps: "app", libs: "lib" };
|
|
1217
1007
|
function getSysRootEntry(file) {
|
|
@@ -1243,13 +1033,7 @@ function getModuleUiWarning(file) {
|
|
|
1243
1033
|
const allowedFileNames = new Set(allowedSuffixes.map((suffix) => `${pascalName}.${suffix}.tsx`));
|
|
1244
1034
|
if (allowedFileNames.has(fileName) || fileName.endsWith(".test.tsx") || fileName.endsWith(".spec.tsx"))
|
|
1245
1035
|
return null;
|
|
1246
|
-
return {
|
|
1247
|
-
rule: "akan.layout.module-ui-file",
|
|
1248
|
-
scope: "layout",
|
|
1249
|
-
severity: "warning",
|
|
1250
|
-
file,
|
|
1251
|
-
message: `Unexpected ${kind} module UI filename "${fileName}". Expected one of: ${[...allowedFileNames].join(", ")}.`
|
|
1252
|
-
};
|
|
1036
|
+
return fileWarning("akan.layout.module-ui-file", "layout", file, `Unexpected ${kind} module UI filename "${fileName}". Expected one of: ${[...allowedFileNames].join(", ")}.`);
|
|
1253
1037
|
}
|
|
1254
1038
|
function getModuleInfo(file) {
|
|
1255
1039
|
const segments = file.split("/");
|
|
@@ -1265,10 +1049,8 @@ function getModuleInfo(file) {
|
|
|
1265
1049
|
if (segments.length !== libIndex + 3)
|
|
1266
1050
|
return null;
|
|
1267
1051
|
const fileName = segments[libIndex + 2];
|
|
1268
|
-
if (moduleName.startsWith("__"))
|
|
1269
|
-
|
|
1270
|
-
return { moduleName: scalarName, fileName, kind: "scalar" };
|
|
1271
|
-
}
|
|
1052
|
+
if (moduleName.startsWith("__"))
|
|
1053
|
+
return { moduleName, fileName, kind: "scalar" };
|
|
1272
1054
|
if (moduleName.startsWith("_"))
|
|
1273
1055
|
return { moduleName, fileName, kind: "service" };
|
|
1274
1056
|
return { moduleName, fileName, kind: "database" };
|
|
@@ -1277,7 +1059,7 @@ function isExportedConst(declaration) {
|
|
|
1277
1059
|
return declaration.exported && ts3.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts3.NodeFlags.Const) !== 0;
|
|
1278
1060
|
}
|
|
1279
1061
|
function isExported(node) {
|
|
1280
|
-
return !!
|
|
1062
|
+
return !!(ts3.getCombinedModifierFlags(node) & ts3.ModifierFlags.Export);
|
|
1281
1063
|
}
|
|
1282
1064
|
function isFunctionLikeInitializer(node) {
|
|
1283
1065
|
return !!node && (ts3.isArrowFunction(node) || ts3.isFunctionExpression(node));
|
|
@@ -1340,24 +1122,16 @@ function compareWarnings(a, b) {
|
|
|
1340
1122
|
// pkgs/@akanjs/cli/quality/quality.script.ts
|
|
1341
1123
|
import { Logger } from "akanjs/common";
|
|
1342
1124
|
|
|
1343
|
-
|
|
1344
|
-
class QualityRunner extends runner("quality") {
|
|
1345
|
-
async scan(workspace) {
|
|
1346
|
-
return await new AkanQualityScanner().scan(workspace.workspaceRoot);
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
// pkgs/@akanjs/cli/quality/quality.script.ts
|
|
1351
|
-
class QualityScript extends script("quality", [QualityRunner]) {
|
|
1125
|
+
class QualityScript extends script("quality") {
|
|
1352
1126
|
async scan(workspace, format = "text") {
|
|
1353
1127
|
const spinner = workspace.spinning("Scanning Akan code quality...");
|
|
1354
|
-
const result = await
|
|
1128
|
+
const result = await new AkanQualityScanner().scan(workspace.workspaceRoot);
|
|
1355
1129
|
spinner.succeed(`Quality scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
|
|
1356
1130
|
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatQualityScanResult(result));
|
|
1357
1131
|
}
|
|
1358
1132
|
async ssr(workspace, format = "text") {
|
|
1359
1133
|
const spinner = workspace.spinning("Measuring Akan server/client render balance...");
|
|
1360
|
-
const scanned = await
|
|
1134
|
+
const scanned = await new AkanQualityScanner().scan(workspace.workspaceRoot);
|
|
1361
1135
|
const result = { ...scanned, warnings: scanned.warnings.filter((warning) => warning.scope === "ssr") };
|
|
1362
1136
|
spinner.succeed(`SSR scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
|
|
1363
1137
|
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSsrScanResult(result));
|
|
@@ -1366,15 +1140,7 @@ class QualityScript extends script("quality", [QualityRunner]) {
|
|
|
1366
1140
|
|
|
1367
1141
|
// pkgs/@akanjs/cli/quality/quality.command.ts
|
|
1368
1142
|
class QualityCommand extends command("quality", [QualityScript], ({ public: target }) => ({
|
|
1369
|
-
quality: target({ desc: "Scan apps and libs for Akan code quality warnings" }).arg("action", String, {
|
|
1370
|
-
desc: "quality action",
|
|
1371
|
-
default: "scan",
|
|
1372
|
-
enum: ["scan", "ssr"]
|
|
1373
|
-
}).option("format", String, {
|
|
1374
|
-
desc: "output format",
|
|
1375
|
-
default: "text",
|
|
1376
|
-
enum: ["text", "json"]
|
|
1377
|
-
}).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) {
|
|
1378
1144
|
if (action === "scan")
|
|
1379
1145
|
await this.qualityScript.scan(workspace, format);
|
|
1380
1146
|
else if (action === "ssr")
|