@akanjs/devkit 3.1.2-rc.3 → 3.2.0-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 +348 -0
- package/abstractDoc.ts +1 -1
- package/agentAssets.ts +2 -2
- package/akanConfig/akanConfig.ts +2 -2
- package/akanMcpContract.ts +2 -2
- package/apiReference.test.ts +37 -0
- package/apiReference.ts +240 -0
- package/applicationBuildReporter.test.ts +54 -0
- package/applicationBuildReporter.ts +12 -0
- package/applicationBuildRunner.test.ts +34 -14
- package/applicationBuildRunner.ts +77 -14
- package/artifact/implicitRootLayout.test.ts +42 -0
- package/artifact/implicitRootLayout.ts +5 -6
- package/artifact/storeBootstrap.ts +24 -0
- package/cloud/cloudApi.test.ts +70 -7
- package/cloud/cloudApi.ts +10 -4
- package/cloud/constants.ts +3 -0
- package/cloud/globalConfig.test.ts +36 -0
- package/cloud/globalConfig.ts +10 -1
- package/codeAgent/skills/akan-debug/SKILL.md +2 -1
- package/codeAgent/skills/akan-page/SKILL.md +30 -2
- package/codeAgent/skills/akan-service/SKILL.md +77 -1
- package/commandDecorators/command.ts +55 -9
- package/commandDecorators/commandDecorators.test.ts +131 -2
- package/declarationReader.ts +211 -0
- package/executors.test.ts +65 -8
- package/executors.ts +5 -3
- package/frameworkGraph.test.ts +121 -0
- package/frameworkGraph.ts +165 -0
- package/frontendBuild/allRoutesBuilder.test.ts +84 -0
- package/frontendBuild/allRoutesBuilder.ts +32 -12
- package/frontendBuild/buildRouteClient.test.ts +192 -2
- package/frontendBuild/bundleDefine.ts +4 -0
- package/frontendBuild/clientBuildTypes.ts +7 -1
- package/frontendBuild/clientEntriesBundler.test.ts +82 -0
- package/frontendBuild/clientEntriesBundler.ts +55 -4
- package/frontendBuild/csrArtifactBuilder.test.ts +23 -0
- package/frontendBuild/csrArtifactBuilder.ts +2 -1
- package/frontendBuild/fontOptimizer.test.ts +54 -0
- package/frontendBuild/fontOptimizer.ts +5 -4
- package/frontendBuild/index.ts +3 -0
- package/frontendBuild/routeClientBuilder.ts +6 -2
- package/frontendBuild/routeModulePreloads.test.ts +59 -0
- package/frontendBuild/routeModulePreloads.ts +52 -0
- package/frontendBuild/ssrBaseArtifactBuilder.ts +35 -10
- package/frontendBuild/ssrDevBundler.test.ts +26 -0
- package/frontendBuild/ssrDevBundler.ts +2 -1
- package/frontendBuild/vendorImportScanner.test.ts +71 -0
- package/frontendBuild/vendorImportScanner.ts +80 -0
- package/frontendBuild/vendorNarrowing.test.ts +103 -0
- package/frontendBuild/vendorNarrowing.ts +64 -0
- package/frontendBuild/vendorSpecifiers.ts +13 -0
- package/gitRevision.test.ts +82 -0
- package/gitRevision.ts +128 -0
- package/incrementalBuilder/builderGraphPayloads.ts +60 -0
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +105 -6
- package/incrementalBuilder/incrementalBuilder.proc.ts +6 -10
- package/integration/devStability.integration.test.ts +1 -1
- package/integration/devStabilityHarness.test.ts +1 -1
- package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +4 -2
- package/lint/__fixtures__/no-js-private-class-method/bad.tsx +2 -0
- package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -2
- package/lint/no-daisyui-legacy-class.grit +9 -7
- package/lint/no-deep-internal-import.grit +1 -1
- package/lint/no-js-private-class-method.grit +9 -28
- package/maintainabilityBaseline.ts +7 -2
- package/maintainabilityContract.test.ts +183 -0
- package/maintainabilityContract.ts +145 -30
- package/maintainabilityExceptionScanner.test.ts +13 -2
- package/maintainabilityExceptionScanner.ts +16 -6
- package/maintainabilityLimits.ts +12 -0
- package/maintainabilityReport.test.ts +35 -0
- package/maintainabilityReport.ts +29 -8
- package/mobile/nativePublicEnv.test.ts +5 -1
- package/package.json +3 -3
- package/routeJsBudget.test.ts +177 -0
- package/routeJsBudget.ts +222 -0
- package/structureScanner.test.ts +167 -0
- package/structureScanner.ts +164 -0
- package/transforms/barrelAnalyzer.ts +10 -3
- package/transforms/barrelImportsPlugin.ts +13 -7
- package/transforms/serverEnvPlugin.test.ts +1 -1
- package/transforms/serverEnvPlugin.ts +1 -1
- package/transforms/transforms.test.ts +64 -12
- package/transforms/tsconfigPackageResolver.test.ts +12 -0
- package/typeChecker.test.ts +100 -0
- package/typeChecker.ts +14 -2
- package/workflow/plan.ts +79 -17
- package/workflow/types.ts +1 -0
- package/workspaceGraph.test.ts +121 -0
- package/workspaceGraph.ts +149 -16
- package/workspaceLayout.ts +1 -0
- /package/integration/{devStabilityHarness.ts → devStabilityHarness.fixture.ts} +0 -0
package/workflow/types.ts
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { AkanChangeMap, AkanGraphEdge, AkanGraphNode } from "@akanjs/language";
|
|
3
|
+
import { WorkspaceGraph } from "./workspaceGraph";
|
|
4
|
+
|
|
5
|
+
const node = (id: string, owner: string | null, change?: AkanGraphNode["change"]): AkanGraphNode => ({
|
|
6
|
+
id,
|
|
7
|
+
kind: id.split(":")[0] as AkanGraphNode["kind"],
|
|
8
|
+
name: id,
|
|
9
|
+
label: id,
|
|
10
|
+
owner,
|
|
11
|
+
module: null,
|
|
12
|
+
detail: [],
|
|
13
|
+
at: null,
|
|
14
|
+
...(change ? { change } : {}),
|
|
15
|
+
});
|
|
16
|
+
const edge = (from: string, to: string, change?: AkanGraphEdge["change"]): AkanGraphEdge => ({
|
|
17
|
+
from,
|
|
18
|
+
to,
|
|
19
|
+
kind: "calls",
|
|
20
|
+
label: null,
|
|
21
|
+
count: 1,
|
|
22
|
+
at: null,
|
|
23
|
+
...(change ? { change } : {}),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("WorkspaceGraph.changesVisibleTo", () => {
|
|
27
|
+
it("keeps the app's and the libs' changes, and an unchanged neighbour only while an edge still reaches it", () => {
|
|
28
|
+
const changes: AkanChangeMap = {
|
|
29
|
+
version: 1,
|
|
30
|
+
nodes: [
|
|
31
|
+
node("endpoint:apps/demo:memo.pin", "apps/demo", "added"),
|
|
32
|
+
node("method:libs/shared:file.touch", "libs/shared"),
|
|
33
|
+
node("method:libs/shared:file.open", "libs/shared"),
|
|
34
|
+
node("component:apps/admin/page/a.tsx#default", "apps/admin", "changed"),
|
|
35
|
+
node("guard:Owner", null, "added"),
|
|
36
|
+
],
|
|
37
|
+
edges: [
|
|
38
|
+
edge("endpoint:apps/demo:memo.pin", "method:libs/shared:file.touch", "added"),
|
|
39
|
+
edge("component:apps/admin/page/a.tsx#default", "method:libs/shared:file.open"),
|
|
40
|
+
],
|
|
41
|
+
since: "0123456789ab",
|
|
42
|
+
summary: { added: 2, removed: 0, changed: 1 },
|
|
43
|
+
unmapped: [
|
|
44
|
+
{ file: "apps/admin/page/b.tsx", change: "added" },
|
|
45
|
+
{ file: "apps/demo/lib/memo/memo.dictionary.ts", change: "changed" },
|
|
46
|
+
{ file: "libs/shared/lib/file/file.md", change: "removed" },
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
const visible = WorkspaceGraph.changesVisibleTo(changes, "demo");
|
|
50
|
+
expect(visible.nodes.map(({ id }) => id)).toEqual([
|
|
51
|
+
"endpoint:apps/demo:memo.pin",
|
|
52
|
+
"method:libs/shared:file.touch",
|
|
53
|
+
"guard:Owner",
|
|
54
|
+
]);
|
|
55
|
+
expect(visible.edges).toHaveLength(1);
|
|
56
|
+
expect(visible.summary).toEqual({ added: 2, removed: 0, changed: 0 });
|
|
57
|
+
expect(visible.unmapped.map(({ file }) => file)).toEqual([
|
|
58
|
+
"apps/demo/lib/memo/memo.dictionary.ts",
|
|
59
|
+
"libs/shared/lib/file/file.md",
|
|
60
|
+
]);
|
|
61
|
+
expect(visible.since).toBe("0123456789ab");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("WorkspaceGraph.answer", () => {
|
|
66
|
+
it("answers a small graph whole, and drops untouched pages and their route-only guards from one past the limit", () => {
|
|
67
|
+
const fillers = Array.from({ length: 597 }, (_, idx) => node(`method:apps/demo:memo.m${idx}`, "apps/demo"));
|
|
68
|
+
const guard = { ...node("guard:libs/shared:signedIn", "libs/shared"), detail: ["route"] };
|
|
69
|
+
const graph = {
|
|
70
|
+
version: 1 as const,
|
|
71
|
+
nodes: [
|
|
72
|
+
node("layout:apps/demo:/", "apps/demo"),
|
|
73
|
+
node("route:apps/demo:/memo", "apps/demo"),
|
|
74
|
+
node("route:apps/demo:/peek", "apps/demo"),
|
|
75
|
+
node("endpoint:apps/demo:memo.pin", "apps/demo", "changed"),
|
|
76
|
+
guard,
|
|
77
|
+
...fillers,
|
|
78
|
+
],
|
|
79
|
+
edges: [
|
|
80
|
+
edge("layout:apps/demo:/", "route:apps/demo:/memo"),
|
|
81
|
+
edge("layout:apps/demo:/", "route:apps/demo:/peek"),
|
|
82
|
+
edge("layout:apps/demo:/", "guard:libs/shared:signedIn"),
|
|
83
|
+
edge("route:apps/demo:/peek", "endpoint:apps/demo:memo.pin"),
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
const small = WorkspaceGraph.answer({ ...graph, nodes: graph.nodes.slice(0, 5) }, {});
|
|
87
|
+
if ("diagnostics" in small) throw new Error("expected an answer");
|
|
88
|
+
expect(small.nodes).toHaveLength(5);
|
|
89
|
+
const unscoped = WorkspaceGraph.answer(graph, {});
|
|
90
|
+
if ("diagnostics" in unscoped) throw new Error("expected an answer");
|
|
91
|
+
expect(unscoped.nodes.slice(0, 2).map(({ id }) => id)).toEqual([
|
|
92
|
+
"route:apps/demo:/peek",
|
|
93
|
+
"endpoint:apps/demo:memo.pin",
|
|
94
|
+
]);
|
|
95
|
+
expect(unscoped.nodes).toHaveLength(599);
|
|
96
|
+
expect(unscoped.edges.map(({ from, to }) => `${from} -> ${to}`)).toEqual([
|
|
97
|
+
"route:apps/demo:/peek -> endpoint:apps/demo:memo.pin",
|
|
98
|
+
]);
|
|
99
|
+
const scoped = WorkspaceGraph.answer(graph, { app: "demo" });
|
|
100
|
+
if ("diagnostics" in scoped) throw new Error("expected an answer");
|
|
101
|
+
expect(scoped.nodes).toHaveLength(602);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("WorkspaceGraph.scope", () => {
|
|
106
|
+
it("keeps a lib's guard without pulling in every app endpoint that uses it", () => {
|
|
107
|
+
const guard = "guard:libs/shared:Admin";
|
|
108
|
+
const own = "endpoint:libs/shared:file.touch";
|
|
109
|
+
const other = "endpoint:apps/demo:memo.pin";
|
|
110
|
+
const scoped = WorkspaceGraph.scope(
|
|
111
|
+
{
|
|
112
|
+
version: 1,
|
|
113
|
+
nodes: [node(guard, "libs/shared"), node(own, "libs/shared"), node(other, "apps/demo")],
|
|
114
|
+
edges: [edge(own, guard), edge(other, guard)],
|
|
115
|
+
},
|
|
116
|
+
{ app: "shared" },
|
|
117
|
+
);
|
|
118
|
+
expect(scoped.nodes.map(({ id }) => id)).toEqual([guard, own]);
|
|
119
|
+
expect(scoped.edges).toEqual([edge(own, guard)]);
|
|
120
|
+
});
|
|
121
|
+
});
|
package/workspaceGraph.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { type AkanGraph, type AkanGraphNode, AkanIndex } from "@akanjs/language";
|
|
3
|
+
import { type AkanChangeMap, type AkanGraph, AkanGraphDiff, type AkanGraphNode, AkanIndex } from "@akanjs/language";
|
|
4
4
|
import ts from "typescript";
|
|
5
5
|
import type { Workspace } from "./commandDecorators";
|
|
6
|
+
import { GitRevision } from "./gitRevision";
|
|
6
7
|
|
|
7
8
|
export interface GraphScope {
|
|
8
9
|
app?: string | null;
|
|
@@ -11,26 +12,88 @@ export interface GraphScope {
|
|
|
11
12
|
|
|
12
13
|
//? `env/` holds each environment's secrets in plain TypeScript; the graph never needs to read it.
|
|
13
14
|
const indexedPath = /^(apps|libs)\/(?!(?:.*\/)?(?:node_modules|dist|\.akan|env)\/).*\.tsx?$/;
|
|
15
|
+
const hiddenPath = /(?:^|\/)(?:node_modules|dist|\.akan|env)\//;
|
|
14
16
|
const unscopedNodeLimit = 600;
|
|
17
|
+
const pageKinds = new Set<AkanGraphNode["kind"]>(["route", "layout"]);
|
|
18
|
+
const cachedBaseLimit = 4;
|
|
15
19
|
const slashed = (fileName: string) => fileName.replace(/\\/g, "/");
|
|
16
20
|
|
|
17
21
|
export class WorkspaceGraph {
|
|
18
|
-
static
|
|
22
|
+
static readonly #bases = new Map<string, Promise<AkanGraph>>();
|
|
23
|
+
|
|
24
|
+
static async build(workspace: Workspace, { digests = false } = {}): Promise<AkanGraph> {
|
|
19
25
|
const root = workspace.workspaceRoot;
|
|
20
|
-
const index = new AkanIndex({ ts, root });
|
|
21
|
-
const texts = new Map<string, string>();
|
|
22
26
|
const files = (await workspace.getAllFiles("{apps,libs}/**/*.{ts,tsx}"))
|
|
23
27
|
.map(slashed)
|
|
24
28
|
.filter((file) => indexedPath.test(file));
|
|
25
|
-
await
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}),
|
|
29
|
+
return WorkspaceGraph.#graphOf(root, await WorkspaceGraph.#textsOf(root, files), digests);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static async #textsOf(root: string, files: string[]) {
|
|
33
|
+
const read = await Promise.all(
|
|
34
|
+
files.map(async (file) => [file, await readFile(path.join(root, file), "utf8").catch(() => null)] as const),
|
|
32
35
|
);
|
|
33
|
-
return
|
|
36
|
+
return new Map(read.filter((entry): entry is readonly [string, string] => entry[1] !== null));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static async changes(workspace: Workspace, since: string | null): Promise<AkanChangeMap | { error: string }> {
|
|
40
|
+
const forkPoint = await new GitRevision(workspace.workspaceRoot).forkPointOf(since);
|
|
41
|
+
if (forkPoint.error !== undefined) return { error: forkPoint.error };
|
|
42
|
+
return await WorkspaceGraph.changesAt(workspace, forkPoint.sha);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static async changesAt(workspace: Workspace, sha: string): Promise<AkanChangeMap> {
|
|
46
|
+
const root = workspace.workspaceRoot;
|
|
47
|
+
const git = new GitRevision(root);
|
|
48
|
+
const [before, after, files] = await Promise.all([
|
|
49
|
+
WorkspaceGraph.#baseAt(root, sha),
|
|
50
|
+
git
|
|
51
|
+
.workingFiles()
|
|
52
|
+
.then((working) =>
|
|
53
|
+
WorkspaceGraph.#textsOf(
|
|
54
|
+
root,
|
|
55
|
+
working.filter((file) => indexedPath.test(file)),
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
.then((texts) => WorkspaceGraph.#graphOf(root, texts, true)),
|
|
59
|
+
git.changedFilesSince(sha),
|
|
60
|
+
]);
|
|
61
|
+
const changes = AkanGraphDiff.between(before, after);
|
|
62
|
+
const mapped = new Set([
|
|
63
|
+
...changes.nodes
|
|
64
|
+
.filter((node) => node.change)
|
|
65
|
+
.flatMap((node) => [
|
|
66
|
+
node.at?.file,
|
|
67
|
+
...(node.fields ?? []).filter((field) => field.change).map(({ at }) => at.file),
|
|
68
|
+
]),
|
|
69
|
+
...changes.edges.filter((edge) => edge.change).map((edge) => edge.at?.file),
|
|
70
|
+
]);
|
|
71
|
+
const unmapped = files.filter(({ file }) => !mapped.has(file) && !hiddenPath.test(file));
|
|
72
|
+
return { ...changes, since: sha.slice(0, 12), unmapped };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static #baseAt(root: string, sha: string) {
|
|
76
|
+
const key = `${root}\n${sha}`;
|
|
77
|
+
const cached = WorkspaceGraph.#bases.get(key);
|
|
78
|
+
if (cached) return cached;
|
|
79
|
+
const graph = new GitRevision(root)
|
|
80
|
+
.filesAt(sha, (file) => indexedPath.test(file))
|
|
81
|
+
.then((texts) => WorkspaceGraph.#graphOf(root, texts, true));
|
|
82
|
+
graph.catch(() => WorkspaceGraph.#bases.delete(key));
|
|
83
|
+
WorkspaceGraph.#bases.set(key, graph);
|
|
84
|
+
for (const old of [...WorkspaceGraph.#bases.keys()].slice(0, -cachedBaseLimit)) WorkspaceGraph.#bases.delete(old);
|
|
85
|
+
return graph;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static #graphOf(root: string, texts: Map<string, string>, digests: boolean) {
|
|
89
|
+
const index = new AkanIndex({ ts, root });
|
|
90
|
+
const byFileName = new Map<string, string>();
|
|
91
|
+
for (const [file, text] of texts) {
|
|
92
|
+
const fileName = slashed(path.join(root, file));
|
|
93
|
+
byFileName.set(fileName, text);
|
|
94
|
+
index.setFile(fileName, text);
|
|
95
|
+
}
|
|
96
|
+
return index.graph((fileName) => byFileName.get(fileName), { digests });
|
|
34
97
|
}
|
|
35
98
|
|
|
36
99
|
static scope(graph: AkanGraph, { app, module }: GraphScope): AkanGraph {
|
|
@@ -41,11 +104,36 @@ export class WorkspaceGraph {
|
|
|
41
104
|
.filter((node) => (!owners || owners.has(node.owner ?? "")) && (!module || node.module === module))
|
|
42
105
|
.map((node) => node.id),
|
|
43
106
|
);
|
|
44
|
-
|
|
107
|
+
//? A lib's guard is linked from every app's endpoints; through it, a scope would reach all of them.
|
|
108
|
+
const guards = new Set(graph.nodes.filter((node) => node.kind === "guard").map((node) => node.id));
|
|
109
|
+
const reaches = (id: string) => seeds.has(id) && !guards.has(id);
|
|
110
|
+
const edges = graph.edges.filter((edge) => reaches(edge.from) || reaches(edge.to));
|
|
45
111
|
const ids = new Set([...seeds, ...edges.flatMap((edge) => [edge.from, edge.to])]);
|
|
46
112
|
return { version: 1, nodes: graph.nodes.filter((node) => ids.has(node.id)), edges };
|
|
47
113
|
}
|
|
48
114
|
|
|
115
|
+
//? Pages are many and most only wrap one another, so a graph too large to answer whole drops the pages that neither
|
|
116
|
+
//? changed nor use a change first, and the route-only guards they leave unlinked.
|
|
117
|
+
static #withoutPages(graph: AkanGraph): AkanGraph {
|
|
118
|
+
const changed = new Set(graph.nodes.filter((node) => node.change).map((node) => node.id));
|
|
119
|
+
const touching = new Set(
|
|
120
|
+
graph.edges
|
|
121
|
+
.filter((edge) => edge.kind !== "wraps" && (changed.has(edge.from) || changed.has(edge.to)))
|
|
122
|
+
.flatMap((edge) => [edge.from, edge.to]),
|
|
123
|
+
);
|
|
124
|
+
const dropped = new Set(
|
|
125
|
+
graph.nodes
|
|
126
|
+
.filter((node) => pageKinds.has(node.kind) && !node.change && !touching.has(node.id))
|
|
127
|
+
.map((node) => node.id),
|
|
128
|
+
);
|
|
129
|
+
const edges = graph.edges.filter((edge) => !dropped.has(edge.from) && !dropped.has(edge.to));
|
|
130
|
+
const linked = new Set(edges.flatMap((edge) => [edge.from, edge.to]));
|
|
131
|
+
const isRouteOnly = (node: AkanGraphNode) =>
|
|
132
|
+
node.kind === "guard" && node.detail.includes("route") && !node.change && !linked.has(node.id);
|
|
133
|
+
const nodes = graph.nodes.filter((node) => !dropped.has(node.id) && !isRouteOnly(node));
|
|
134
|
+
return { version: 1, nodes, edges };
|
|
135
|
+
}
|
|
136
|
+
|
|
49
137
|
static visibleTo(graph: AkanGraph, app: string): AkanGraph {
|
|
50
138
|
const appOwner = `apps/${app}`;
|
|
51
139
|
const isVisible = ({ owner }: AkanGraphNode) => !owner || owner === appOwner || owner.startsWith("libs/");
|
|
@@ -54,16 +142,61 @@ export class WorkspaceGraph {
|
|
|
54
142
|
return { version: 1, nodes, edges: graph.edges.filter((edge) => ids.has(edge.from) && ids.has(edge.to)) };
|
|
55
143
|
}
|
|
56
144
|
|
|
57
|
-
static
|
|
145
|
+
static changesVisibleTo(changes: AkanChangeMap, app: string): AkanChangeMap {
|
|
146
|
+
const visible = WorkspaceGraph.visibleTo(changes, app);
|
|
147
|
+
const linked = new Set(visible.edges.flatMap((edge) => [edge.from, edge.to]));
|
|
148
|
+
const nodes = visible.nodes.filter((node) => node.change || linked.has(node.id));
|
|
149
|
+
const appPath = `apps/${app}/`;
|
|
150
|
+
return {
|
|
151
|
+
...changes,
|
|
152
|
+
nodes,
|
|
153
|
+
edges: visible.edges,
|
|
154
|
+
summary: WorkspaceGraph.#summaryOf(nodes),
|
|
155
|
+
unmapped: changes.unmapped.filter(({ file }) => file.startsWith(appPath) || file.startsWith("libs/")),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
static #summaryOf(nodes: AkanGraphNode[]) {
|
|
160
|
+
const summary = { added: 0, removed: 0, changed: 0 };
|
|
161
|
+
for (const { change } of nodes) if (change) summary[change] += 1;
|
|
162
|
+
return summary;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
static answerChanges(changes: AkanChangeMap | { error: string }, scope: GraphScope) {
|
|
166
|
+
if ("error" in changes)
|
|
167
|
+
return {
|
|
168
|
+
diagnostics: [{ severity: "error", code: "graph-since-unknown", message: changes.error, input: "since" }],
|
|
169
|
+
};
|
|
170
|
+
const answer = WorkspaceGraph.answer(
|
|
171
|
+
changes,
|
|
172
|
+
scope,
|
|
173
|
+
changes.nodes.filter((node) => node.change),
|
|
174
|
+
);
|
|
175
|
+
if ("diagnostics" in answer) return { since: changes.since, summary: changes.summary, ...answer };
|
|
176
|
+
const summary = WorkspaceGraph.#summaryOf(answer.nodes);
|
|
177
|
+
const owners = scope.app ? [`apps/${scope.app}/`, `libs/${scope.app}/`] : null;
|
|
178
|
+
const modules = scope.module ? [`/lib/${scope.module}/`, `/lib/_${scope.module}/`] : null;
|
|
179
|
+
const unmapped = changes.unmapped.filter(
|
|
180
|
+
({ file }) =>
|
|
181
|
+
(!owners || owners.some((owner) => file.startsWith(owner))) &&
|
|
182
|
+
(!modules || modules.some((segment) => file.includes(segment))),
|
|
183
|
+
);
|
|
184
|
+
return { since: changes.since, summary, ...answer, unmapped };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
static answer(graph: AkanGraph, scope: GraphScope, counted = graph.nodes) {
|
|
58
188
|
if (scope.app || scope.module || graph.nodes.length <= unscopedNodeLimit) return WorkspaceGraph.scope(graph, scope);
|
|
189
|
+
const unscoped = WorkspaceGraph.#withoutPages(graph);
|
|
190
|
+
if (unscoped.nodes.length <= unscopedNodeLimit) return unscoped;
|
|
59
191
|
const owners = new Map<string, number>();
|
|
60
|
-
for (const node of
|
|
192
|
+
for (const node of counted)
|
|
193
|
+
if (node.owner && !pageKinds.has(node.kind)) owners.set(node.owner, (owners.get(node.owner) ?? 0) + 1);
|
|
61
194
|
return {
|
|
62
195
|
diagnostics: [
|
|
63
196
|
{
|
|
64
197
|
severity: "error",
|
|
65
198
|
code: "graph-scope-required",
|
|
66
|
-
message: `The graph has ${
|
|
199
|
+
message: `The graph has ${unscoped.nodes.length} nodes besides its pages. Pass app, module or both to scope it.`,
|
|
67
200
|
input: "app",
|
|
68
201
|
},
|
|
69
202
|
],
|
package/workspaceLayout.ts
CHANGED
|
File without changes
|