@akanjs/devkit 3.1.1-rc.9 → 3.1.2-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +117 -0
- package/agentAssets.test.ts +65 -0
- package/agentAssets.ts +166 -0
- package/agentsIndex.ts +4 -4
- package/akanApp/akanApp.host.test.ts +12 -0
- package/akanApp/akanApp.host.ts +12 -0
- package/akanConfig/akanConfig.test.ts +105 -0
- package/akanConfig/akanConfig.ts +21 -1
- package/akanConfig/productionLockfile.test.ts +31 -0
- package/akanConfig/productionLockfile.ts +2 -0
- package/akanContext.ts +129 -4
- package/akanMcpContract.test.ts +28 -1
- package/akanMcpContract.ts +51 -3
- package/applicationBuildRunner.test.ts +53 -0
- package/applicationBuildRunner.ts +7 -4
- package/artifact/implicitRootLayout.ts +4 -2
- package/biome.base.json +4 -1
- package/cloud/globalConfig.test.ts +34 -0
- package/cloud/globalConfig.ts +2 -2
- package/codeAgent/agent/AkanCodeEnv.ts +26 -0
- package/codeAgent/agent/AkanCodeServices.ts +5 -2
- package/codeAgent/agent/CodeAgent.ts +11 -0
- package/codeAgent/agent/CodeAgentGate.ts +156 -10
- package/codeAgent/agent/CodeAgentProxy.ts +5 -4
- package/codeAgent/agent/CodeAgentRpcHost.ts +3 -0
- package/codeAgent/agent/CodeAgentRpcListener.ts +102 -16
- package/codeAgent/agent/SubagentPool.ts +5 -0
- package/codeAgent/agent/WorkspaceTrust.ts +200 -0
- package/codeAgent/agent/akanCodePaths.ts +8 -1
- package/codeAgent/agent/akanSystemPrompt.ts +5 -1
- package/codeAgent/agent/codeAgent.test.ts +41 -2
- package/codeAgent/agent/codeAgentContract.test.ts +119 -0
- package/codeAgent/agent/codeAgentHosts.ts +30 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +53 -1
- package/codeAgent/agent/codeAgentRpcListener.test.ts +115 -13
- package/codeAgent/agent/stringArg.ts +1 -1
- package/codeAgent/feedback/PreviewFeedback.ts +2 -2
- package/codeAgent/feedback/VerifyFeedback.ts +3 -4
- package/codeAgent/index.ts +2 -2
- package/codeAgent/skills/akan-agent/SKILL.md +43 -0
- package/codeAgent/skills/akan-code/SKILL.md +38 -11
- package/codeAgent/skills/akan-debug/SKILL.md +41 -6
- package/codeAgent/skills/akan-deps/SKILL.md +1 -2
- package/codeAgent/skills/akan-err/SKILL.md +2 -1
- package/codeAgent/skills/akan-mcp/SKILL.md +40 -0
- package/codeAgent/skills/akan-page/SKILL.md +44 -0
- package/codeAgent/skills/akan-query/SKILL.md +39 -0
- package/codeAgent/skills/akan-runtime/SKILL.md +43 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +16 -6
- package/codeAgent/skills/akan-transport/SKILL.md +40 -0
- package/codeAgent/skills/akan-validate/SKILL.md +13 -11
- package/codeAgent/tools/AkanCli.ts +2 -1
- package/codeAgent/tools/AkanCodePlugins.ts +5 -1
- package/codeAgent/tools/AkanEditScope.ts +8 -3
- package/codeAgent/tools/AkanToolPack.ts +4 -10
- package/codeAgent/tools/AkanVerifier.ts +13 -31
- package/codeAgent/tools/McpServerConfig.ts +6 -4
- package/codeAgent/tools/McpSignIn.ts +7 -7
- package/codeAgent/tools/McpTokenStore.ts +63 -16
- package/codeAgent/tools/McpToolPack.ts +8 -1
- package/codeAgent/tools/WebToolPack.ts +50 -6
- package/codeAgent/tools/akanCodePlugins.test.ts +130 -60
- package/codeAgent/tools/mcpOAuth.test.ts +50 -14
- package/codeAgent/tools/mcpServerConfig.test.ts +130 -1
- package/commandDecorators/command.ts +4 -1
- package/commandDecorators/workspaceVersionCheck.test.ts +17 -7
- package/commandDecorators/workspaceVersionCheck.ts +4 -1
- package/commentDensityScanner.test.ts +121 -0
- package/commentDensityScanner.ts +101 -0
- package/componentSizeScanner.test.ts +36 -0
- package/componentSizeScanner.ts +52 -0
- package/componentWriteChainScanner.test.ts +137 -0
- package/componentWriteChainScanner.ts +104 -0
- package/conventionSet.test.ts +73 -0
- package/conventionSet.ts +67 -0
- package/createTunnel.test.ts +200 -2
- package/createTunnel.ts +80 -6
- package/csrE2e/ssrRegistry.e2e.test.ts +6 -1
- package/deprecatedCommentScanner.test.ts +54 -0
- package/deprecatedCommentScanner.ts +30 -0
- package/endpointReach.test.ts +58 -0
- package/endpointReach.ts +80 -0
- package/envSeed.test.ts +90 -0
- package/envSeed.ts +68 -0
- package/executors.test.ts +19 -5
- package/executors.ts +59 -4
- package/fileSys.test.ts +30 -2
- package/fileSys.ts +14 -0
- package/formSetterScanner.test.ts +44 -1
- package/formSetterScanner.ts +53 -8
- package/frontendBuild/csrArtifactBuilder.ts +7 -0
- package/frontendBuild/mcpAppViewBuilder.test.ts +63 -0
- package/frontendBuild/mcpAppViewBuilder.ts +7 -1
- package/frontendBuild/optionalPeers.test.ts +132 -0
- package/index.ts +1 -0
- package/integration/devStability.integration.test.ts +9 -5
- package/integration/ssrMemoryProbe.ts +6 -0
- package/kindBranchScanner.test.ts +89 -0
- package/kindBranchScanner.ts +158 -0
- package/lint/__fixtures__/no-oversized-list-limit/bad.tsx +18 -0
- package/lint/__fixtures__/no-oversized-list-limit/good.tsx +23 -0
- package/lint/no-oversized-list-limit.grit +73 -0
- package/lint/no-unguarded-endpoint.grit +5 -5
- package/linter.test.ts +12 -0
- package/linter.ts +35 -12
- package/maintainabilityBaseline.test.ts +280 -0
- package/maintainabilityBaseline.ts +220 -0
- package/maintainabilityContract.test.ts +846 -0
- package/maintainabilityContract.ts +404 -0
- package/maintainabilityExceptionScanner.test.ts +90 -0
- package/maintainabilityExceptionScanner.ts +85 -0
- package/maintainabilityLimits.ts +60 -0
- package/maintainabilityReport.test.ts +130 -0
- package/maintainabilityReport.ts +153 -0
- package/mobile/desktopRelease.test.ts +1 -1
- package/mobile/desktopRelease.ts +3 -1
- package/mobile/desktopServerStage.test.ts +1 -0
- package/mobile/desktopServerStage.ts +3 -1
- package/mobile/nativeApp.test.ts +20 -5
- package/mobile/nativeApp.ts +4 -1
- package/mobile/nativeConfig.test.ts +209 -5
- package/mobile/nativeConfig.ts +127 -6
- package/mobile/nativePublicEnv.test.ts +2 -0
- package/mobile/nativePublicEnv.ts +13 -0
- package/package.json +4 -2
- package/platformTest/LinuxDockerTarget.ts +3 -0
- package/{codeAgent/feedback → preview}/PreviewChrome.ts +13 -7
- package/preview/PreviewView.ts +219 -0
- package/preview/index.ts +2 -0
- package/preview/previewView.test.ts +76 -0
- package/prompter.ts +7 -0
- package/qualityScanner.test.ts +75 -0
- package/qualityScanner.ts +17 -1
- package/routeSourceValidator.test.ts +11 -2
- package/routeSourceValidator.ts +14 -2
- package/serviceLoopCallScanner.test.ts +56 -0
- package/serviceLoopCallScanner.ts +122 -0
- package/shellOnlyEnv.test.ts +65 -0
- package/shellOnlyEnv.ts +54 -0
- package/signalMutationIndex.test.ts +15 -0
- package/signalMutationIndex.ts +23 -82
- package/sourceComments.test.ts +45 -0
- package/sourceComments.ts +70 -0
- package/sshHostKeys.ts +59 -0
- package/storeAccessScanner.test.ts +84 -0
- package/storeAccessScanner.ts +71 -0
- package/storeSource.ts +160 -0
- package/storeWriteScanner.test.ts +292 -0
- package/storeWriteScanner.ts +50 -0
- package/subspace.test.ts +10 -0
- package/subspaceClone.ts +3 -1
- package/subspaceConfig.ts +6 -0
- package/testOnlyExportScanner.test.ts +83 -0
- package/testOnlyExportScanner.ts +124 -0
- package/transforms/barrelImportsPlugin.ts +10 -4
- package/transforms/mcpAppPageTrimmer.test.ts +8 -2
- package/transforms/mcpAppPageTrimmer.ts +5 -20
- package/transforms/pureLazyMarker.ts +1 -1
- package/typeChecker.ts +32 -0
- package/typecheck/projectTypecheck.test.ts +18 -0
- package/typecheck/projectTypecheck.ts +29 -5
- package/typecheck/typecheck.proc.ts +6 -0
- package/typecheck/typecheckFailure.ts +13 -0
- package/types.ts +1 -0
- package/workflow/artifacts.ts +16 -2
- package/workflow/executor.ts +22 -7
- package/workflow/guardCatalog.test.ts +59 -0
- package/workflow/guardCatalog.ts +108 -0
- package/workflow/index.ts +4 -0
- package/workflow/interactionScaffold.ts +122 -0
- package/workflow/mutationScaffold.ts +533 -0
- package/workflow/mutationSource.test.ts +108 -0
- package/workflow/mutationSource.ts +219 -0
- package/workflow/plan.ts +82 -0
- package/workflow/render.ts +19 -4
- package/workflow/source.test.ts +185 -0
- package/workflow/source.ts +151 -24
- package/workflow/sourceAst.ts +10 -48
- package/workflow/types.ts +18 -1
- package/workspaceLayout.ts +2 -0
- package/writePath.test.ts +109 -0
- package/writePath.ts +219 -0
- package/writePlacementScanner.test.ts +77 -0
- package/writePlacementScanner.ts +50 -0
- package/writingActionIndex.test.ts +99 -0
- package/writingActionIndex.ts +63 -0
- package/codeAgent/feedback/PreviewView.ts +0 -95
- /package/{codeAgent/feedback → preview}/previewChrome.test.ts +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { SizeViolation } from "./maintainabilityContract";
|
|
3
|
+
import { MaintainabilityReport } from "./maintainabilityReport";
|
|
4
|
+
|
|
5
|
+
const fileOf = (file: string, lineCount = 450): SizeViolation => ({
|
|
6
|
+
kind: "size",
|
|
7
|
+
rule: "akan.size.file",
|
|
8
|
+
key: file,
|
|
9
|
+
file,
|
|
10
|
+
line: 1,
|
|
11
|
+
lineCount,
|
|
12
|
+
maxLines: 400,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const fixLinesOf = (message: string) => message.split("How to fix:\n")[1]?.split("\n") ?? [];
|
|
16
|
+
|
|
17
|
+
describe("MaintainabilityReport", () => {
|
|
18
|
+
test("gives a client file, a ui/ or webkit/ file, a long component and a busy one the same ordered paths", () => {
|
|
19
|
+
const message = MaintainabilityReport.describe("apps/demo", [
|
|
20
|
+
fileOf("lib/court/Court.Zone.tsx"),
|
|
21
|
+
fileOf("ui/Board.tsx"),
|
|
22
|
+
fileOf("webkit/useBoard.tsx"),
|
|
23
|
+
{
|
|
24
|
+
...fileOf("lib/court/Court.Zone.tsx", 300),
|
|
25
|
+
rule: "akan.size.component",
|
|
26
|
+
key: "lib/court/Court.Zone.tsx#Canvas",
|
|
27
|
+
line: 4,
|
|
28
|
+
maxLines: 250,
|
|
29
|
+
component: "Canvas",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
kind: "storeAccess",
|
|
33
|
+
rule: "akan.component.store-access",
|
|
34
|
+
key: "lib/court/Court.Zone.tsx#Canvas",
|
|
35
|
+
file: "lib/court/Court.Zone.tsx",
|
|
36
|
+
line: 4,
|
|
37
|
+
component: "Canvas",
|
|
38
|
+
accessCount: 20,
|
|
39
|
+
maxAccesses: 16,
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
const [fix = "", ...rest] = fixLinesOf(message);
|
|
43
|
+
const order = ["mode/kind table", "webkit/use<X>.tsx", "`common/`", "add-mutation", "`lib/<sub>`"].map((path) =>
|
|
44
|
+
fix.indexOf(path),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(rest).toEqual([]);
|
|
48
|
+
expect(fix).toStartWith(
|
|
49
|
+
" *.Zone.tsx, ui/**, webkit/**, component or hook, component or hook reaching the store often: ",
|
|
50
|
+
);
|
|
51
|
+
expect(order.every((idx) => idx > 0)).toBe(true);
|
|
52
|
+
expect([...order].sort((a, b) => a - b)).toEqual(order);
|
|
53
|
+
expect(fix).toContain('`{ [kind in cnst.X["value"]]: … }`');
|
|
54
|
+
expect(fix).toContain("akan workflow plan create-interaction");
|
|
55
|
+
expect(fix).toContain("never model-bound code moved into `ui/`");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("names a write sent from the wrong place, a call per item and a raised entry with their own fixes", () => {
|
|
59
|
+
const message = MaintainabilityReport.describe("apps/demo", [
|
|
60
|
+
{
|
|
61
|
+
kind: "baselineGrowth",
|
|
62
|
+
rule: "akan.baseline.growth",
|
|
63
|
+
key: "lib/court/court.store.ts#assign",
|
|
64
|
+
file: "akan.baseline.json",
|
|
65
|
+
line: 5,
|
|
66
|
+
debtRule: "akan.store.multi-write",
|
|
67
|
+
count: 3,
|
|
68
|
+
committed: 2,
|
|
69
|
+
reference: "3f2a9c1d0b7e",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
kind: "writePlacement",
|
|
73
|
+
rule: "akan.write.placement",
|
|
74
|
+
key: "webkit/useBoard.tsx",
|
|
75
|
+
file: "webkit/useBoard.tsx",
|
|
76
|
+
line: 7,
|
|
77
|
+
writes: ["st.do.removeBoard", "fetch.placeBoard"],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
kind: "loopCall",
|
|
81
|
+
rule: "akan.service.call-in-loop",
|
|
82
|
+
key: "lib/court/court.service.ts#syncAll",
|
|
83
|
+
file: "lib/court/court.service.ts",
|
|
84
|
+
line: 12,
|
|
85
|
+
owner: "syncAll",
|
|
86
|
+
calls: ["this.pcService.getPc"],
|
|
87
|
+
recorded: 0,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
kind: "multiWrite",
|
|
91
|
+
rule: "akan.component.write-chain",
|
|
92
|
+
key: "webkit/useBoard.tsx#useBoard",
|
|
93
|
+
file: "webkit/useBoard.tsx",
|
|
94
|
+
line: 9,
|
|
95
|
+
owner: "useBoard",
|
|
96
|
+
writes: ["st.do.removeBoard (per item)"],
|
|
97
|
+
writeCount: 2,
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
expect(message).toContain(
|
|
102
|
+
'akan.baseline.growth entry "lib/court/court.store.ts#assign" under akan.store.multi-write allows 3 writes, above the 2 at 3f2a9c1d0b7e',
|
|
103
|
+
);
|
|
104
|
+
expect(message).toContain(
|
|
105
|
+
"akan.write.placement sends 2 server write(s) from outside a store action or a module component: st.do.removeBoard, fetch.placeBoard",
|
|
106
|
+
);
|
|
107
|
+
expect(message).toContain(
|
|
108
|
+
"akan.service.call-in-loop service method syncAll calls once per item: this.pcService.getPc, more than the 0 recorded",
|
|
109
|
+
);
|
|
110
|
+
expect(message).toContain("hook useBoard sends a write per item from one handler: st.do.removeBoard (per item)");
|
|
111
|
+
expect(fixLinesOf(message).map((line) => line.split(":")[0])).toEqual([
|
|
112
|
+
" baseline entry",
|
|
113
|
+
" write sent outside a store action or module component",
|
|
114
|
+
" service call per item",
|
|
115
|
+
" handler with several writes",
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("keeps the server files' own fixes", () => {
|
|
120
|
+
const message = MaintainabilityReport.describe("apps/demo", [
|
|
121
|
+
fileOf("lib/court/Court.View.tsx"),
|
|
122
|
+
fileOf("lib/court/court.service.ts", 520),
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
expect(fixLinesOf(message)).toEqual([
|
|
126
|
+
" *.View.tsx: Move model-independent pieces into ui/ components and display logic onto Light<Model>.",
|
|
127
|
+
" *.service.ts: Move state transitions into document chain methods, and split an unrelated responsibility into its own module.",
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { AbstractDoc } from "./abstractDoc";
|
|
2
|
+
import type {
|
|
3
|
+
DebtRuleId,
|
|
4
|
+
MaintainabilityCommands,
|
|
5
|
+
MaintainabilityFailure,
|
|
6
|
+
MaintainabilityRuleId,
|
|
7
|
+
} from "./maintainabilityContract";
|
|
8
|
+
import { type FileKind, MaintainabilityLimits } from "./maintainabilityLimits";
|
|
9
|
+
|
|
10
|
+
type FixKey = FileKind | Exclude<MaintainabilityRuleId, "akan.size.file">;
|
|
11
|
+
|
|
12
|
+
export class MaintainabilityReport {
|
|
13
|
+
static readonly #clientSplitFix =
|
|
14
|
+
'Split it, in this order: a mode/kind table `{ [kind in cnst.X["value"]]: … }` in place of mode flags, each entry a component of the module owning the data of that kind; an interaction hook in `webkit/use<X>.tsx` with its pure logic in `common/` (`akan workflow plan create-interaction`); the writes behind one endpoint (`akan workflow plan add-mutation`); last, a sub-concept in its own module (`lib/<sub>` or `lib/_<feature>`), never model-bound code moved into `ui/`.';
|
|
15
|
+
static readonly #serverMarkupFix =
|
|
16
|
+
"Move model-independent pieces into ui/ components and display logic onto Light<Model>.";
|
|
17
|
+
static readonly #oneEndpointFix =
|
|
18
|
+
"Make it one service method that runs the writes inside `this.transaction(fn)`, with a side effect such as a push command in `this.afterCommit(fn)`, behind one endpoint the action calls once; a write per item becomes one endpoint taking the list. An MCP agent or another screen calling one endpoint alone skips the rest.";
|
|
19
|
+
static readonly #fixes: { [key in FixKey]: string } = {
|
|
20
|
+
".Template.tsx": MaintainabilityReport.#clientSplitFix,
|
|
21
|
+
".Unit.tsx": MaintainabilityReport.#serverMarkupFix,
|
|
22
|
+
".Util.tsx": MaintainabilityReport.#clientSplitFix,
|
|
23
|
+
".View.tsx": MaintainabilityReport.#serverMarkupFix,
|
|
24
|
+
".Zone.tsx": MaintainabilityReport.#clientSplitFix,
|
|
25
|
+
"ui/": MaintainabilityReport.#clientSplitFix,
|
|
26
|
+
"webkit/": MaintainabilityReport.#clientSplitFix,
|
|
27
|
+
".store.ts":
|
|
28
|
+
"Move business logic into the service behind an endpoint and derived values onto Light<Model>; an action is fetch, set, toast.",
|
|
29
|
+
".document.ts":
|
|
30
|
+
"Move display and predicate logic onto Light<Model>, and split an unrelated concept into its own module.",
|
|
31
|
+
".service.ts":
|
|
32
|
+
"Move state transitions into document chain methods, and split an unrelated responsibility into its own module.",
|
|
33
|
+
[AbstractDoc.suffix]:
|
|
34
|
+
"Trim the abstract to a title, one sentence naming what the module owns, and a `## Rules` list of the invariants the code cannot show.",
|
|
35
|
+
"akan.size.component": MaintainabilityReport.#clientSplitFix,
|
|
36
|
+
"akan.store.multi-write": MaintainabilityReport.#oneEndpointFix,
|
|
37
|
+
"akan.store.root-write-chain": `A store may call another through \`RootStore\`, but that does not make the writes one. ${MaintainabilityReport.#oneEndpointFix}`,
|
|
38
|
+
"akan.component.write-chain":
|
|
39
|
+
"Move the writes into one service method behind one endpoint, called by one store action the handler calls once; a component chaining `st.do` writes, or writing once per item, leaves an agent or another screen half-done.",
|
|
40
|
+
"akan.component.store-access": MaintainabilityReport.#clientSplitFix,
|
|
41
|
+
"akan.write.placement":
|
|
42
|
+
"A server write leaves the browser through one store action, started by one module component. Move a `fetch.<mutation>` into a store action, and hand `ui/` and `webkit/` code the write as an `onX` prop or hook argument from the module's `Zone` or `Util`: they stay free of any one model, and an agent or another screen reaches the write through the store.",
|
|
43
|
+
"akan.service.call-in-loop":
|
|
44
|
+
"Load the set once (`load<Model>Many(ids)`, one filter query) or write it once (`create<Model>Many(rows)`, one batch method), then loop in memory: a call per item is a round trip per item, so the endpoint slows as its input grows.",
|
|
45
|
+
"akan.baseline.growth":
|
|
46
|
+
"Restore the entry from the default branch, then split the file or move the writes instead: `akan lint` lowers an entry as the code shrinks and never raises one.",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
static readonly #labels: { [key in FixKey]?: string } = {
|
|
50
|
+
"ui/": "ui/**",
|
|
51
|
+
"webkit/": "webkit/**",
|
|
52
|
+
"akan.size.component": "component or hook",
|
|
53
|
+
"akan.store.multi-write": "store action with several writes",
|
|
54
|
+
"akan.store.root-write-chain": "store method chaining other stores' writes",
|
|
55
|
+
"akan.component.write-chain": "handler with several writes",
|
|
56
|
+
"akan.component.store-access": "component or hook reaching the store often",
|
|
57
|
+
"akan.write.placement": "write sent outside a store action or module component",
|
|
58
|
+
"akan.service.call-in-loop": "service call per item",
|
|
59
|
+
"akan.baseline.growth": "baseline entry",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
static readonly #units: { [rule in DebtRuleId]: string } = {
|
|
63
|
+
"akan.size.file": "lines",
|
|
64
|
+
"akan.size.component": "lines",
|
|
65
|
+
"akan.component.store-access": "store accesses",
|
|
66
|
+
"akan.store.multi-write": "writes",
|
|
67
|
+
"akan.store.root-write-chain": "writes",
|
|
68
|
+
"akan.component.write-chain": "writes",
|
|
69
|
+
"akan.write.placement": "writes",
|
|
70
|
+
"akan.service.call-in-loop": "calls per item",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
static describe(sysPath: string, failures: MaintainabilityFailure[], commands: MaintainabilityCommands = {}) {
|
|
74
|
+
const groups = new Map<string, { labels: string[]; rules: Set<MaintainabilityRuleId> }>();
|
|
75
|
+
for (const failure of failures) {
|
|
76
|
+
const fixKey = MaintainabilityReport.#fixKeyOf(failure);
|
|
77
|
+
const fix = MaintainabilityReport.#fixes[fixKey];
|
|
78
|
+
const group = groups.get(fix) ?? { labels: [], rules: new Set() };
|
|
79
|
+
const label = MaintainabilityReport.#labelOf(fixKey);
|
|
80
|
+
if (!group.labels.includes(label)) group.labels.push(label);
|
|
81
|
+
group.rules.add(failure.rule);
|
|
82
|
+
groups.set(fix, group);
|
|
83
|
+
}
|
|
84
|
+
return [
|
|
85
|
+
`[maintainability] ${failures.length} violation(s) in ${sysPath}:`,
|
|
86
|
+
...failures.map(
|
|
87
|
+
(failure) =>
|
|
88
|
+
` ${sysPath}/${failure.file}:${failure.line} ${failure.rule} ${MaintainabilityReport.#problemOf(failure)}`,
|
|
89
|
+
),
|
|
90
|
+
"How to fix:",
|
|
91
|
+
...[...groups].flatMap(([fix, { labels, rules }]) => [
|
|
92
|
+
` ${labels.join(", ")}: ${fix}`,
|
|
93
|
+
...[...new Set([...rules].flatMap((rule) => commands[rule] ?? []))].map((command) => ` Path: ${command}`),
|
|
94
|
+
]),
|
|
95
|
+
].join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static entriesOf(sysPath: string, failures: MaintainabilityFailure[], commands: MaintainabilityCommands = {}) {
|
|
99
|
+
return failures.map((failure) => ({
|
|
100
|
+
file: `${sysPath}/${failure.file}`,
|
|
101
|
+
line: failure.line,
|
|
102
|
+
rule: failure.rule,
|
|
103
|
+
problem: MaintainabilityReport.#problemOf(failure),
|
|
104
|
+
fix: MaintainabilityReport.#fixes[MaintainabilityReport.#fixKeyOf(failure)],
|
|
105
|
+
path: commands[failure.rule]?.[0] ?? null,
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
static #problemOf(failure: MaintainabilityFailure) {
|
|
110
|
+
if (failure.kind === "baselineGrowth") {
|
|
111
|
+
const entry = `entry "${failure.key}" under ${failure.debtRule}`;
|
|
112
|
+
return failure.committed === undefined
|
|
113
|
+
? `${entry} is not in the baseline at ${failure.reference}, which may only shrink`
|
|
114
|
+
: `${entry} allows ${failure.count} ${MaintainabilityReport.#units[failure.debtRule]}, above the ${failure.committed} at ${failure.reference}; the baseline may only shrink`;
|
|
115
|
+
}
|
|
116
|
+
const grown = failure.recorded === undefined ? "" : `, more than the ${failure.recorded} recorded`;
|
|
117
|
+
if (failure.kind === "multiWrite") {
|
|
118
|
+
const writes = failure.writes.some((write) => write.endsWith("(per item)"))
|
|
119
|
+
? "a write per item"
|
|
120
|
+
: `${failure.writes.length} writes`;
|
|
121
|
+
const listed = `${failure.writes.join(", ")}${grown}`;
|
|
122
|
+
if (failure.rule === "akan.component.write-chain")
|
|
123
|
+
return `${MaintainabilityReport.#unitOf(failure.owner)} ${failure.owner} sends ${writes} from one handler: ${listed}`;
|
|
124
|
+
if (failure.rule === "akan.store.root-write-chain")
|
|
125
|
+
return `store method ${failure.owner} chains ${writes} through other stores from the browser: ${listed}`;
|
|
126
|
+
return `store action ${failure.owner} sends ${writes} from the browser: ${listed}`;
|
|
127
|
+
}
|
|
128
|
+
if (failure.kind === "writePlacement")
|
|
129
|
+
return `sends ${failure.writes.length} server write(s) from outside a store action or a module component: ${failure.writes.join(", ")}${grown}`;
|
|
130
|
+
if (failure.kind === "loopCall")
|
|
131
|
+
return `service method ${failure.owner} calls once per item: ${failure.calls.join(", ")}${grown}`;
|
|
132
|
+
if (failure.kind === "storeAccess")
|
|
133
|
+
return `${MaintainabilityReport.#unitOf(failure.component)} ${failure.component} reaches the store ${failure.accessCount} times, over the ${failure.maxAccesses}-access limit${failure.recorded === undefined ? "" : `, up from ${failure.recorded}`}`;
|
|
134
|
+
const subject = failure.component
|
|
135
|
+
? `${MaintainabilityReport.#unitOf(failure.component)} ${failure.component} has`
|
|
136
|
+
: "file has";
|
|
137
|
+
const up = failure.recorded === undefined ? "" : `, up from ${failure.recorded}`;
|
|
138
|
+
return `${subject} ${failure.lineCount} lines, over the ${failure.maxLines}-line limit${up}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
static #unitOf(name: string) {
|
|
142
|
+
return /^use[A-Z]/.test(name) ? "hook" : "component";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
static #fixKeyOf(failure: MaintainabilityFailure): FixKey {
|
|
146
|
+
if (failure.rule !== "akan.size.file") return failure.rule;
|
|
147
|
+
return MaintainabilityLimits.fileKindOf(failure.file) ?? "akan.size.component";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static #labelOf(fixKey: FixKey) {
|
|
151
|
+
return MaintainabilityReport.#labels[fixKey] ?? `*${fixKey}`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -381,7 +381,7 @@ describe("DesktopRelease signer", () => {
|
|
|
381
381
|
|
|
382
382
|
const windowsTool = (powershell: (out: string) => Promise<number>) =>
|
|
383
383
|
recordingTool(async (cmd, env) => {
|
|
384
|
-
if (cmd[0] === "tar.exe") {
|
|
384
|
+
if (cmd[0] === path.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe")) {
|
|
385
385
|
await Bun.write(path.join(extractedTo(cmd), cmd.at(-1) ?? ""), "MZ");
|
|
386
386
|
return { code: 0, output: "" };
|
|
387
387
|
}
|
package/mobile/desktopRelease.ts
CHANGED
|
@@ -245,7 +245,9 @@ Set-Content -LiteralPath $env:AKAN_SIGNER_OUT -Value $subject -Encoding UTF8`;
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
static async #subjectOf(run: SignatureTool, archive: string, tmp: string, exe: string) {
|
|
248
|
-
|
|
248
|
+
//? Windows' own bsdtar: a GNU tar ahead of it on PATH (Git's) takes `C:` for a remote host.
|
|
249
|
+
const tar = path.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe");
|
|
250
|
+
await run([tar, "-xzf", archive, "-C", tmp, exe]);
|
|
249
251
|
const extracted = path.join(tmp, exe);
|
|
250
252
|
if (!(await Bun.file(extracted).exists())) return { signer: null, unread: `the archive holds no ${exe}` };
|
|
251
253
|
const out = path.join(tmp, "signer.txt");
|
|
@@ -2,6 +2,7 @@ import { cp, mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { toError } from "akanjs/common";
|
|
4
4
|
import type { ServerPolicy } from "akanjs/native/api";
|
|
5
|
+
import { BUILD_ENV_FILE } from "akanjs/server/lifecycle/buildEnv";
|
|
5
6
|
import type { AkanAppConfig, NativeEnv } from "../akanConfig";
|
|
6
7
|
import type { App } from "../commandDecorators";
|
|
7
8
|
import type { PackageJson } from "../types";
|
|
@@ -26,9 +27,10 @@ export interface DesktopServerRun extends ServerPolicy {
|
|
|
26
27
|
export class DesktopServerStage {
|
|
27
28
|
//? An API-only server starts neither the RSC worker nor the akan console and serves no CSR, public/ or SSR
|
|
28
29
|
//? artifact, and `install` wrote its own package.json and bun.lock. The rest of dist is the server's: its chunks,
|
|
29
|
-
//? a `.node`, a `.wasm`.
|
|
30
|
+
//? a `.node`, a `.wasm`. The build's env file would hand it the PORT and AKAN_LOG_TO_FILE its launcher leaves out.
|
|
30
31
|
static readonly uncarried = new Set([
|
|
31
32
|
"Dockerfile",
|
|
33
|
+
BUILD_ENV_FILE,
|
|
32
34
|
"package.json",
|
|
33
35
|
"bun.lock",
|
|
34
36
|
"rscWorker.js",
|
package/mobile/nativeApp.test.ts
CHANGED
|
@@ -313,18 +313,33 @@ describe("NativeApp", () => {
|
|
|
313
313
|
expect(await injected()).toContain('"indexPath":"/mobile"');
|
|
314
314
|
});
|
|
315
315
|
|
|
316
|
-
test("builds its config from the app's locales
|
|
316
|
+
test("builds its config from the app's locales, the plugins' native contributions and the inlined public env", async () => {
|
|
317
317
|
const push: AkanPlugin = { name: "push-notification", native: { permission: "push", plugins: ["push"] } };
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
318
|
+
const root = await makeTempRoot();
|
|
319
|
+
const portal = { ...fakeApp([push, { name: "other" }]), dist: { cwdPath: path.join(root, "dist/apps/portal") } };
|
|
320
|
+
await CsrArtifactBuilder.writePublicEnv(portal as unknown as App, {
|
|
321
|
+
"process.env.AKAN_PUBLIC_APP_NAME": '"portal"',
|
|
322
|
+
"process.env.AKAN_PUBLIC_ENV": '"main"',
|
|
323
|
+
"process.env.AKAN_PUBLIC_SERVE_DOMAIN": '"akamir.com"',
|
|
324
|
+
"process.env.AKAN_PUBLIC_SERVER_URL": '"http://192.168.0.5:8080/api"',
|
|
325
|
+
});
|
|
326
|
+
const app = new NativeApp(portal as unknown as App, target({ basePath: undefined, permissions: ["push"] }));
|
|
322
327
|
|
|
323
328
|
const { config, warnings } = await app.config({ platform: "android" });
|
|
324
329
|
|
|
325
330
|
expect(warnings).toEqual([]);
|
|
326
331
|
expect(config.plugins).toEqual([...NativeConfig.basePlugins, "push"]);
|
|
327
332
|
expect(config.web.dir).toBe(path.join(nativeDir, "default/web"));
|
|
333
|
+
const csp = config.security?.csp as Record<string, string[]> | undefined;
|
|
334
|
+
expect(csp?.["connect-src"]).toEqual([
|
|
335
|
+
"'self'",
|
|
336
|
+
"blob:",
|
|
337
|
+
"data:",
|
|
338
|
+
"https:",
|
|
339
|
+
"wss:",
|
|
340
|
+
"http://192.168.0.5:8080",
|
|
341
|
+
"ws://192.168.0.5:8080",
|
|
342
|
+
]);
|
|
328
343
|
});
|
|
329
344
|
|
|
330
345
|
test("a config nothing is built from (the update key's) stages no desktop bin", async () => {
|
package/mobile/nativeApp.ts
CHANGED
|
@@ -124,10 +124,11 @@ export class NativeApp {
|
|
|
124
124
|
stageBin?: boolean;
|
|
125
125
|
arch?: DesktopArch;
|
|
126
126
|
}) {
|
|
127
|
-
const [appConfig, plugins, nativePlugins] = await Promise.all([
|
|
127
|
+
const [appConfig, plugins, nativePlugins, publicEnv] = await Promise.all([
|
|
128
128
|
this.app.getConfig(),
|
|
129
129
|
this.app.collectPlugins(),
|
|
130
130
|
NativePluginFolders.of(this.app),
|
|
131
|
+
NativePublicEnv.inlined(this.app),
|
|
131
132
|
]);
|
|
132
133
|
const desktop = platform === "macos" || platform === "windows" || platform === "linux";
|
|
133
134
|
const carried = desktop && stageBin ? await new DesktopBin(this.app, appConfig).stage(this.binDir, arch) : [];
|
|
@@ -137,9 +138,11 @@ export class NativeApp {
|
|
|
137
138
|
webDir: this.web.dir,
|
|
138
139
|
contributions: plugins.flatMap((plugin) => (plugin.native ? [plugin.native] : [])),
|
|
139
140
|
locales: appConfig.i18n.locales,
|
|
141
|
+
i18n: appConfig.i18n,
|
|
140
142
|
env: this.env,
|
|
141
143
|
platform,
|
|
142
144
|
nativePlugins,
|
|
145
|
+
publicEnv,
|
|
143
146
|
...(server ? { desktopServer: server } : {}),
|
|
144
147
|
...(carried.length ? { desktopBin: this.binDir } : {}),
|
|
145
148
|
});
|