akanjs 3.1.2-rc.2 → 3.1.2-rc.3
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/common/akanGraph.ts +57 -0
- package/common/index.ts +9 -0
- package/package.json +1 -1
- package/server/akanApp.ts +7 -5
- package/server/akanServer.ts +1 -0
- package/server/artifact/builderRpc.ts +11 -8
- package/server/artifact/ipcTypes.ts +17 -1
- package/server/devtools/devtoolsRouter.ts +15 -0
- package/server/devtools/studio/studioDocument.tsx +202 -0
- package/server/devtools/studio/studioLayout.ts +224 -0
- package/server/devtools/studio/studioRoute.ts +73 -0
- package/server/devtools/studio/studioRuntime.ts +65 -0
- package/server/devtools/studio/studioStyle.ts +39 -0
- package/server/webRouter.ts +4 -0
- package/types/common/akanGraph.d.ts +41 -0
- package/types/common/index.d.ts +1 -0
- package/types/constant/constantRegistry.d.ts +1 -1
- package/types/server/artifact/builderRpc.d.ts +2 -1
- package/types/server/artifact/ipcTypes.d.ts +23 -1
- package/types/server/devtools/devtoolsRouter.d.ts +2 -0
- package/types/server/devtools/studio/studioDocument.d.ts +22 -0
- package/types/server/devtools/studio/studioLayout.d.ts +37 -0
- package/types/server/devtools/studio/studioRoute.d.ts +16 -0
- package/types/server/devtools/studio/studioRuntime.d.ts +1 -0
- package/types/server/devtools/studio/studioStyle.d.ts +1 -0
- package/types/server/migration/migrationCommand.d.ts +2 -2
- package/types/server/migration/migrations.d.ts +1 -1
- package/types/server/webRouter.d.ts +2 -1
- package/types/ui/recipe/inputRecipe.d.ts +2 -2
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type AkanGraphNodeKind =
|
|
2
|
+
| "model"
|
|
3
|
+
| "scalar"
|
|
4
|
+
| "enum"
|
|
5
|
+
| "query"
|
|
6
|
+
| "slice"
|
|
7
|
+
| "endpoint"
|
|
8
|
+
| "guard"
|
|
9
|
+
| "method"
|
|
10
|
+
| "action"
|
|
11
|
+
| "component";
|
|
12
|
+
|
|
13
|
+
export type AkanGraphEdgeKind = "field" | "guard" | "calls" | "fetches" | "uses";
|
|
14
|
+
|
|
15
|
+
export interface AkanGraphLocation {
|
|
16
|
+
file: string;
|
|
17
|
+
line: number;
|
|
18
|
+
column: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AkanGraphField {
|
|
22
|
+
name: string;
|
|
23
|
+
type: string | null;
|
|
24
|
+
array: boolean;
|
|
25
|
+
optional: boolean;
|
|
26
|
+
ref: string | null;
|
|
27
|
+
declaredIn: string;
|
|
28
|
+
at: AkanGraphLocation;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AkanGraphNode {
|
|
32
|
+
id: string;
|
|
33
|
+
kind: AkanGraphNodeKind;
|
|
34
|
+
name: string;
|
|
35
|
+
label: string;
|
|
36
|
+
owner: string | null;
|
|
37
|
+
module: string | null;
|
|
38
|
+
detail: string[];
|
|
39
|
+
at: AkanGraphLocation | null;
|
|
40
|
+
fields?: AkanGraphField[];
|
|
41
|
+
values?: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AkanGraphEdge {
|
|
45
|
+
from: string;
|
|
46
|
+
to: string;
|
|
47
|
+
kind: AkanGraphEdgeKind;
|
|
48
|
+
label: string | null;
|
|
49
|
+
count: number;
|
|
50
|
+
at: AkanGraphLocation | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface AkanGraph {
|
|
54
|
+
version: 1;
|
|
55
|
+
nodes: AkanGraphNode[];
|
|
56
|
+
edges: AkanGraphEdge[];
|
|
57
|
+
}
|
package/common/index.ts
CHANGED
package/package.json
CHANGED
package/server/akanApp.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { getApiPrefix, getWsPrefix, normalizeRoutePrefix, resetEnvCache } from "
|
|
|
9
9
|
import { CrossSiteGuard } from "../signal/CrossSiteGuard";
|
|
10
10
|
import { isTraceEnabled } from "../signal/trace";
|
|
11
11
|
import { AKAN_CHILD_HOST, makeAkanChildProxyHeaders } from "./akanAppHeaders";
|
|
12
|
-
import type {
|
|
12
|
+
import type { BuilderMessage, BuilderRequest, BuilderResponse } from "./artifact";
|
|
13
13
|
import { type AssetHeaderOptions, applySecurityHeaders, assetHeaders } from "./assetHeaders";
|
|
14
14
|
import { compressResponse, compressStreamResponse, encodedFileResponse } from "./contentEncoding";
|
|
15
15
|
import { isPortInUseError } from "./lifecycle/portInUse";
|
|
@@ -1150,6 +1150,7 @@ export class AkanApp {
|
|
|
1150
1150
|
return;
|
|
1151
1151
|
case "build-route":
|
|
1152
1152
|
case "build-csr":
|
|
1153
|
+
case "build-graph":
|
|
1153
1154
|
this.#forwardBuilderReq(idx, message);
|
|
1154
1155
|
return;
|
|
1155
1156
|
}
|
|
@@ -1169,6 +1170,7 @@ export class AkanApp {
|
|
|
1169
1170
|
return;
|
|
1170
1171
|
case "build-route-res":
|
|
1171
1172
|
case "build-csr-res":
|
|
1173
|
+
case "build-graph-res":
|
|
1172
1174
|
this.#forwardBuilderRes(message);
|
|
1173
1175
|
return;
|
|
1174
1176
|
}
|
|
@@ -1354,13 +1356,13 @@ export class AkanApp {
|
|
|
1354
1356
|
}
|
|
1355
1357
|
}
|
|
1356
1358
|
|
|
1357
|
-
#forwardBuilderReq(childIdx: number, message:
|
|
1359
|
+
#forwardBuilderReq(childIdx: number, message: BuilderRequest) {
|
|
1358
1360
|
const gatewayReqId = this.#nextBuilderReqId++;
|
|
1359
1361
|
this.#builderReqMap.set(gatewayReqId, { childIdx, childLocalId: message.id });
|
|
1360
|
-
process.send?.({ ...message, id: gatewayReqId } satisfies
|
|
1362
|
+
process.send?.({ ...message, id: gatewayReqId } satisfies BuilderRequest);
|
|
1361
1363
|
}
|
|
1362
1364
|
|
|
1363
|
-
#forwardBuilderRes(message:
|
|
1365
|
+
#forwardBuilderRes(message: BuilderResponse) {
|
|
1364
1366
|
const request = this.#builderReqMap.get(message.id);
|
|
1365
1367
|
if (!request) {
|
|
1366
1368
|
this.logger.warn(`No child found for ${message.type} id=${message.id}`);
|
|
@@ -1369,7 +1371,7 @@ export class AkanApp {
|
|
|
1369
1371
|
this.#builderReqMap.delete(message.id);
|
|
1370
1372
|
const child = this.#children.get(request.childIdx);
|
|
1371
1373
|
if (!child || child.proc.killed) return;
|
|
1372
|
-
this.#sendToChild(child, { ...message, id: request.childLocalId } satisfies
|
|
1374
|
+
this.#sendToChild(child, { ...message, id: request.childLocalId } satisfies BuilderResponse);
|
|
1373
1375
|
}
|
|
1374
1376
|
|
|
1375
1377
|
#fanoutToFederation(message: AkanIpcMessage | BuilderMessage, exceptIdx?: number) {
|
package/server/akanServer.ts
CHANGED
|
@@ -782,6 +782,7 @@ export class AkanServer {
|
|
|
782
782
|
websocketPrefix: this.websocketPrefix,
|
|
783
783
|
openapi: this.openapi,
|
|
784
784
|
getStatus: () => this.status,
|
|
785
|
+
buildGraph: webRouter ? () => webRouter.buildGraph() : undefined,
|
|
785
786
|
}).createRoutes();
|
|
786
787
|
return { ...openapiRoutes, ...mcpRoutes, ...devtoolsRoutes, ...soloRoutes };
|
|
787
788
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { Logger } from "akanjs/common";
|
|
2
2
|
import type {
|
|
3
|
-
BuilderCsrReq,
|
|
4
|
-
BuilderCsrRes,
|
|
5
3
|
BuilderEvent,
|
|
6
4
|
BuilderMessage,
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
BuilderRequest,
|
|
6
|
+
BuilderResponse,
|
|
7
|
+
BuildGraphPayload,
|
|
9
8
|
BuildRouteClientResult,
|
|
10
9
|
BuildRouteResultPayload,
|
|
11
10
|
CsrUpdatedPayload,
|
|
@@ -31,7 +30,7 @@ export class BuilderRpc {
|
|
|
31
30
|
readonly #logger = new Logger("BuilderRpc");
|
|
32
31
|
readonly #pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
|
|
33
32
|
readonly #offMessage: () => void;
|
|
34
|
-
readonly #send: (msg:
|
|
33
|
+
readonly #send: (msg: BuilderRequest) => void;
|
|
35
34
|
#nextId = 1;
|
|
36
35
|
#disposed = false;
|
|
37
36
|
|
|
@@ -40,13 +39,13 @@ export class BuilderRpc {
|
|
|
40
39
|
throw new Error("[builder] process.send unavailable — backend must be spawned by the CLI with ipc enabled");
|
|
41
40
|
this.#send = process.send.bind(process);
|
|
42
41
|
this.#offMessage = this.#listen((msg) => {
|
|
43
|
-
if (msg.type === "build-route-res" || msg.type === "build-csr-res") {
|
|
44
|
-
const res = msg as
|
|
42
|
+
if (msg.type === "build-route-res" || msg.type === "build-csr-res" || msg.type === "build-graph-res") {
|
|
43
|
+
const res = msg as BuilderResponse;
|
|
45
44
|
const waiter = this.#pending.get(res.id);
|
|
46
45
|
if (!waiter) return;
|
|
47
46
|
this.#pending.delete(res.id);
|
|
48
47
|
if (!res.ok) waiter.reject(new Error(`[builder] ${res.type} failed: ${res.error}`));
|
|
49
|
-
else waiter.resolve(res.type === "build-
|
|
48
|
+
else waiter.resolve(res.type === "build-csr-res" ? undefined : res.data);
|
|
50
49
|
return;
|
|
51
50
|
}
|
|
52
51
|
const ev = msg as BuilderEvent;
|
|
@@ -114,6 +113,10 @@ export class BuilderRpc {
|
|
|
114
113
|
);
|
|
115
114
|
}
|
|
116
115
|
|
|
116
|
+
async buildGraph(): Promise<BuildGraphPayload> {
|
|
117
|
+
return await this.#request<BuildGraphPayload>("build-graph", (id) => this.#send({ type: "build-graph", id }));
|
|
118
|
+
}
|
|
119
|
+
|
|
117
120
|
static #timeoutMs(): number {
|
|
118
121
|
const configured = Number(process.env.AKAN_BUILDER_RPC_TIMEOUT_MS);
|
|
119
122
|
if (Number.isFinite(configured) && configured > 0) return configured;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AkanGraph } from "akanjs/common";
|
|
1
2
|
import type { SsrManifest } from "../ssrTypes";
|
|
2
3
|
|
|
3
4
|
export interface BuildRouteResultPayload {
|
|
@@ -80,6 +81,17 @@ export type BuilderCsrRes =
|
|
|
80
81
|
| { type: "build-csr-res"; id: number; ok: true }
|
|
81
82
|
| { type: "build-csr-res"; id: number; ok: false; error: string };
|
|
82
83
|
|
|
84
|
+
export interface BuildGraphPayload {
|
|
85
|
+
workspaceRoot: string;
|
|
86
|
+
graph: AkanGraph;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type BuilderGraphReq = { type: "build-graph"; id: number };
|
|
90
|
+
|
|
91
|
+
export type BuilderGraphRes =
|
|
92
|
+
| { type: "build-graph-res"; id: number; ok: true; data: BuildGraphPayload }
|
|
93
|
+
| { type: "build-graph-res"; id: number; ok: false; error: string };
|
|
94
|
+
|
|
83
95
|
/** Drain and exit (the only way bundler memory returns to the OS); draining, not killing, keeps a rebuild whole. */
|
|
84
96
|
export type BuilderControl = { type: "builder-shutdown"; reason: string };
|
|
85
97
|
|
|
@@ -164,7 +176,11 @@ export type BuilderEvent =
|
|
|
164
176
|
/** The boot builds settled (the SSR registry's, and CSR's when the env arms it): their workers are a boot's peak. */
|
|
165
177
|
| { type: "boot-armed" };
|
|
166
178
|
|
|
167
|
-
export type
|
|
179
|
+
export type BuilderRequest = BuilderReq | BuilderCsrReq | BuilderGraphReq;
|
|
180
|
+
|
|
181
|
+
export type BuilderResponse = BuilderRes | BuilderCsrRes | BuilderGraphRes;
|
|
182
|
+
|
|
183
|
+
export type BuilderMessage = BuilderRequest | BuilderResponse | BuilderControl | BuilderEvent;
|
|
168
184
|
|
|
169
185
|
export interface ClientManifestEntry {
|
|
170
186
|
id: string;
|
|
@@ -2,11 +2,13 @@ import type { BaseEnv } from "akanjs/base";
|
|
|
2
2
|
import { Logger } from "akanjs/common";
|
|
3
3
|
import { DictionaryRegistry, type RootDictionary } from "akanjs/dictionary";
|
|
4
4
|
import { isLocalDevRuntime } from "../../signal/trace";
|
|
5
|
+
import type { BuildGraphPayload } from "../artifact";
|
|
5
6
|
import type { DiLifecycle } from "../di/diLifecycle";
|
|
6
7
|
import type { HttpRoutes } from "../types";
|
|
7
8
|
import { ConstantSerializer } from "./constantSerializer";
|
|
8
9
|
import { DepsSerializer } from "./depsSerializer";
|
|
9
10
|
import { SignalSerializer } from "./signalSerializer";
|
|
11
|
+
import { StudioRoute } from "./studio/studioRoute";
|
|
10
12
|
import type { DevtoolsApp, DevtoolsEnvelope, DevtoolsIndex, DictionaryData } from "./types";
|
|
11
13
|
|
|
12
14
|
export interface DevtoolsRouterContext {
|
|
@@ -18,6 +20,7 @@ export interface DevtoolsRouterContext {
|
|
|
18
20
|
websocketPrefix: string;
|
|
19
21
|
openapi: boolean;
|
|
20
22
|
getStatus: () => string;
|
|
23
|
+
buildGraph?: () => Promise<BuildGraphPayload | null>;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
/** Builtin routes, not signals: off `/api` and guards, and never in the `serializedSignal` payload sent to clients. */
|
|
@@ -41,7 +44,14 @@ export class DevtoolsRouter {
|
|
|
41
44
|
|
|
42
45
|
createRoutes(): HttpRoutes {
|
|
43
46
|
if (!DevtoolsRouter.isEnabled(this.#context.env)) return {};
|
|
47
|
+
const studio = new StudioRoute({
|
|
48
|
+
basePath: `${DevtoolsRouter.basePath}/studio`,
|
|
49
|
+
appName: this.#context.env.appName,
|
|
50
|
+
buildGraph: this.#context.buildGraph ?? null,
|
|
51
|
+
});
|
|
44
52
|
return {
|
|
53
|
+
[`${DevtoolsRouter.basePath}/studio`]: { GET: (req: Request) => this.#gated(() => studio.page(req)) },
|
|
54
|
+
[`${DevtoolsRouter.basePath}/studio/graph`]: { GET: () => this.#gated(() => studio.graph()) },
|
|
45
55
|
[`${DevtoolsRouter.basePath}/devtools`]: { GET: () => DevtoolsRouter.#json(this.#index()) },
|
|
46
56
|
[`${DevtoolsRouter.basePath}/constant`]: {
|
|
47
57
|
GET: () => this.#respond("constant", () => ConstantSerializer.serialize()),
|
|
@@ -100,6 +110,11 @@ export class DevtoolsRouter {
|
|
|
100
110
|
}
|
|
101
111
|
}
|
|
102
112
|
|
|
113
|
+
async #gated(respond: () => Promise<Response>): Promise<Response> {
|
|
114
|
+
if (!DevtoolsRouter.isEnabled(this.#context.env)) return DevtoolsRouter.#json({ error: "Not found" }, 404);
|
|
115
|
+
return await respond();
|
|
116
|
+
}
|
|
117
|
+
|
|
103
118
|
#envelope<Kind extends string, Data>(kind: Kind, data: Data): DevtoolsEnvelope<Kind, Data> {
|
|
104
119
|
return { kind, version: 1, generatedAt: new Date().toISOString(), app: this.#app(), data };
|
|
105
120
|
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { AkanGraphLocation } from "akanjs/common";
|
|
2
|
+
import type { BuildGraphPayload } from "../../artifact";
|
|
3
|
+
import { type StudioBox, StudioLayout, type StudioScene, type StudioView } from "./studioLayout";
|
|
4
|
+
import { studioRuntime } from "./studioRuntime";
|
|
5
|
+
import { studioStyle } from "./studioStyle";
|
|
6
|
+
|
|
7
|
+
export type StudioEditor = "vscode" | "cursor";
|
|
8
|
+
|
|
9
|
+
export interface StudioQuery {
|
|
10
|
+
view: StudioView;
|
|
11
|
+
module: string | null;
|
|
12
|
+
editor: StudioEditor;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface StudioDocumentProps {
|
|
16
|
+
basePath: string;
|
|
17
|
+
appName: string;
|
|
18
|
+
payload: BuildGraphPayload;
|
|
19
|
+
query: StudioQuery;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const editorLabels: { [editor in StudioEditor]: string } = { vscode: "VS Code", cursor: "Cursor" };
|
|
23
|
+
const viewLabels: { [view in StudioView]: string } = { erd: "ERD", signals: "Signals" };
|
|
24
|
+
const charWidth = 7.3;
|
|
25
|
+
const kindShownIn: { [view in StudioView]: Set<string> } = {
|
|
26
|
+
erd: new Set(["model", "scalar", "enum"]),
|
|
27
|
+
signals: new Set(["endpoint", "slice", "query", "model"]),
|
|
28
|
+
};
|
|
29
|
+
const clip = (text: string, max: number) => (text.length > max ? `${text.slice(0, max - 1)}…` : text);
|
|
30
|
+
|
|
31
|
+
export class StudioDocument {
|
|
32
|
+
static render({ basePath, appName, payload, query }: StudioDocumentProps) {
|
|
33
|
+
const modules = StudioLayout.modulesOf(payload.graph);
|
|
34
|
+
const scene =
|
|
35
|
+
query.view === "erd"
|
|
36
|
+
? StudioLayout.erd(payload.graph, query.module)
|
|
37
|
+
: StudioLayout.signals(payload.graph, query.module ?? modules[0] ?? null);
|
|
38
|
+
const hrefOf = (next: Partial<StudioQuery>) => StudioDocument.href(basePath, { ...query, ...next });
|
|
39
|
+
const openOf = (at: AkanGraphLocation | null) =>
|
|
40
|
+
at ? StudioDocument.editorHref(query.editor, payload.workspaceRoot, at) : undefined;
|
|
41
|
+
return (
|
|
42
|
+
<html lang="en">
|
|
43
|
+
<head>
|
|
44
|
+
<meta charSet="utf-8" />
|
|
45
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
46
|
+
<title>{`Akan Studio · ${appName}`}</title>
|
|
47
|
+
<style>{studioStyle}</style>
|
|
48
|
+
</head>
|
|
49
|
+
<body>
|
|
50
|
+
<header>
|
|
51
|
+
<strong>Akan Studio</strong>
|
|
52
|
+
<span className="muted">{appName}</span>
|
|
53
|
+
<nav>
|
|
54
|
+
{(["erd", "signals"] as const).map((view) => (
|
|
55
|
+
<a key={view} href={hrefOf({ view })} aria-current={view === query.view ? "page" : undefined}>
|
|
56
|
+
{viewLabels[view]}
|
|
57
|
+
</a>
|
|
58
|
+
))}
|
|
59
|
+
</nav>
|
|
60
|
+
<form method="get" action={basePath}>
|
|
61
|
+
<input type="hidden" name="view" value={query.view} />
|
|
62
|
+
<input type="hidden" name="editor" value={query.editor} />
|
|
63
|
+
<select name="module" defaultValue={query.module ?? ""} data-studio-module="">
|
|
64
|
+
{query.view === "erd" ? <option value="">All modules</option> : null}
|
|
65
|
+
{modules.map((module) => (
|
|
66
|
+
<option key={module} value={module}>
|
|
67
|
+
{module}
|
|
68
|
+
</option>
|
|
69
|
+
))}
|
|
70
|
+
</select>
|
|
71
|
+
<noscript>
|
|
72
|
+
<button type="submit">Show</button>
|
|
73
|
+
</noscript>
|
|
74
|
+
</form>
|
|
75
|
+
<input type="search" placeholder="Filter by name" data-studio-filter="" />
|
|
76
|
+
<span className="muted">{`${scene.boxes.length} nodes · ${scene.links.length} edges`}</span>
|
|
77
|
+
<nav className="end">
|
|
78
|
+
{(["vscode", "cursor"] as const).map((editor) => (
|
|
79
|
+
<a key={editor} href={hrefOf({ editor })} aria-current={editor === query.editor ? "true" : undefined}>
|
|
80
|
+
{editorLabels[editor]}
|
|
81
|
+
</a>
|
|
82
|
+
))}
|
|
83
|
+
<a href={`${basePath}/graph`}>JSON</a>
|
|
84
|
+
</nav>
|
|
85
|
+
</header>
|
|
86
|
+
<main>
|
|
87
|
+
{scene.boxes.length ? (
|
|
88
|
+
StudioDocument.#svg(scene, query.view, openOf)
|
|
89
|
+
) : (
|
|
90
|
+
<p className="empty">Nothing to draw here. Pick another module or view.</p>
|
|
91
|
+
)}
|
|
92
|
+
</main>
|
|
93
|
+
{StudioDocument.#runtime()}
|
|
94
|
+
</body>
|
|
95
|
+
</html>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static unavailable(appName: string, reason: string) {
|
|
100
|
+
return (
|
|
101
|
+
<html lang="en">
|
|
102
|
+
<head>
|
|
103
|
+
<meta charSet="utf-8" />
|
|
104
|
+
<title>{`Akan Studio · ${appName}`}</title>
|
|
105
|
+
<style>{studioStyle}</style>
|
|
106
|
+
</head>
|
|
107
|
+
<body>
|
|
108
|
+
<p className="empty">{reason}</p>
|
|
109
|
+
{StudioDocument.#runtime()}
|
|
110
|
+
</body>
|
|
111
|
+
</html>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
static #runtime() {
|
|
116
|
+
return (
|
|
117
|
+
|
|
118
|
+
<script dangerouslySetInnerHTML={{ __html: `(${studioRuntime.toString()})();` }} />
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static href(basePath: string, { view, module, editor }: StudioQuery): string {
|
|
123
|
+
const search = new URLSearchParams({ view, editor });
|
|
124
|
+
if (module) search.set("module", module);
|
|
125
|
+
return `${basePath}?${search.toString()}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
static editorHref(editor: StudioEditor, workspaceRoot: string, at: AkanGraphLocation): string {
|
|
129
|
+
const root = workspaceRoot.replace(/\\/g, "/").replace(/\/$/, "");
|
|
130
|
+
const file = `${root.startsWith("/") ? "" : "/"}${root}/${at.file}`;
|
|
131
|
+
return `${editor}://file${encodeURI(file)}:${at.line}:${at.column}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static #svg(scene: StudioScene, view: StudioView, openOf: (at: AkanGraphLocation | null) => string | undefined) {
|
|
135
|
+
return (
|
|
136
|
+
<svg data-studio="" width={scene.width} height={scene.height} viewBox={`0 0 ${scene.width} ${scene.height}`}>
|
|
137
|
+
<defs>
|
|
138
|
+
<marker id="arrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto">
|
|
139
|
+
<path d="M0,0 L8,4 L0,8 z" className="arrow" />
|
|
140
|
+
</marker>
|
|
141
|
+
</defs>
|
|
142
|
+
{scene.columns.map((column) =>
|
|
143
|
+
column.label ? (
|
|
144
|
+
<text key={column.x} className="column" x={column.x} y={24}>
|
|
145
|
+
{column.label}
|
|
146
|
+
</text>
|
|
147
|
+
) : null,
|
|
148
|
+
)}
|
|
149
|
+
<g>
|
|
150
|
+
{scene.links.map(({ edge, path }) => (
|
|
151
|
+
<path
|
|
152
|
+
key={`${edge.from}>${edge.to}>${edge.kind}>${edge.label ?? ""}`}
|
|
153
|
+
d={path}
|
|
154
|
+
className={`link ${edge.kind}`}
|
|
155
|
+
data-from={edge.from}
|
|
156
|
+
data-to={edge.to}
|
|
157
|
+
markerEnd="url(#arrow)"
|
|
158
|
+
>
|
|
159
|
+
<title>{`${edge.kind} ${edge.label ?? ""}${edge.count > 1 ? ` ×${edge.count}` : ""}`}</title>
|
|
160
|
+
</path>
|
|
161
|
+
))}
|
|
162
|
+
</g>
|
|
163
|
+
<g>{scene.boxes.map((box) => StudioDocument.#box(box, view, openOf))}</g>
|
|
164
|
+
</svg>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
static #box(
|
|
169
|
+
{ node, x, y, width, height, rows }: StudioBox,
|
|
170
|
+
view: StudioView,
|
|
171
|
+
openOf: (at: AkanGraphLocation | null) => string | undefined,
|
|
172
|
+
) {
|
|
173
|
+
const lib = node.owner?.startsWith("libs/") ? node.owner.slice(5) : null;
|
|
174
|
+
const kind = [lib, kindShownIn[view].has(node.kind) ? node.kind : null].filter(Boolean).join(" · ");
|
|
175
|
+
const chars = Math.floor((width - 24) / charWidth);
|
|
176
|
+
return (
|
|
177
|
+
<g key={node.id} className={`box ${node.kind}`} data-node={node.id} data-label={node.label}>
|
|
178
|
+
<title>{node.at ? `${node.label}\n${node.at.file}:${node.at.line}` : node.label}</title>
|
|
179
|
+
<rect x={x} y={y} width={width} height={height} rx={6} />
|
|
180
|
+
<rect className="bar" x={x} y={y} width={4} height={height} rx={2} />
|
|
181
|
+
<a href={openOf(node.at)}>
|
|
182
|
+
<text className="title" x={x + 12} y={y + 18}>
|
|
183
|
+
{clip(node.label, kind ? chars - kind.length - 1 : chars)}
|
|
184
|
+
</text>
|
|
185
|
+
</a>
|
|
186
|
+
<text className="kind" x={x + width - 10} y={y + 18} textAnchor="end">
|
|
187
|
+
{kind}
|
|
188
|
+
</text>
|
|
189
|
+
{rows.map((row, idx) => (
|
|
190
|
+
<a key={idx} href={openOf(row.at)}>
|
|
191
|
+
<text x={x + 12} y={row.y + 4}>
|
|
192
|
+
{clip(row.text, row.hint ? chars - row.hint.length - 1 : chars)}
|
|
193
|
+
</text>
|
|
194
|
+
<text className="hint" x={x + width - 10} y={row.y + 4} textAnchor="end">
|
|
195
|
+
{row.hint}
|
|
196
|
+
</text>
|
|
197
|
+
</a>
|
|
198
|
+
))}
|
|
199
|
+
</g>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { AkanGraph, AkanGraphEdge, AkanGraphLocation, AkanGraphNode, AkanGraphNodeKind } from "akanjs/common";
|
|
2
|
+
|
|
3
|
+
export type StudioView = "erd" | "signals";
|
|
4
|
+
|
|
5
|
+
export interface StudioRow {
|
|
6
|
+
text: string;
|
|
7
|
+
hint: string;
|
|
8
|
+
y: number;
|
|
9
|
+
at: AkanGraphLocation | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface StudioBox {
|
|
13
|
+
node: AkanGraphNode;
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
rows: StudioRow[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface StudioLink {
|
|
22
|
+
edge: AkanGraphEdge;
|
|
23
|
+
path: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface StudioColumn {
|
|
27
|
+
label: string;
|
|
28
|
+
x: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface StudioScene {
|
|
32
|
+
boxes: StudioBox[];
|
|
33
|
+
links: StudioLink[];
|
|
34
|
+
columns: StudioColumn[];
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const columnGap = 112;
|
|
40
|
+
const boxGap = 16;
|
|
41
|
+
const headerHeight = 28;
|
|
42
|
+
const rowHeight = 18;
|
|
43
|
+
const padding = 24;
|
|
44
|
+
const columnTop = 40;
|
|
45
|
+
const maxColumnHeight = 1800;
|
|
46
|
+
const typeKinds = new Set<AkanGraphNodeKind>(["model", "scalar", "enum"]);
|
|
47
|
+
const flowColumns: { label: string; kinds: AkanGraphNodeKind[] }[] = [
|
|
48
|
+
{ label: "Components", kinds: ["component"] },
|
|
49
|
+
{ label: "Store actions", kinds: ["action"] },
|
|
50
|
+
{ label: "Endpoints & slices", kinds: ["endpoint", "slice"] },
|
|
51
|
+
{ label: "Service methods", kinds: ["method"] },
|
|
52
|
+
{ label: "Queries & models", kinds: ["query", "model"] },
|
|
53
|
+
];
|
|
54
|
+
const flowColumnOf = new Map(flowColumns.flatMap(({ kinds }, idx) => kinds.map((kind) => [kind, idx] as const)));
|
|
55
|
+
const flowEdgeKinds = new Set(["calls", "fetches", "uses"]);
|
|
56
|
+
|
|
57
|
+
export class StudioLayout {
|
|
58
|
+
static modulesOf(graph: AkanGraph): string[] {
|
|
59
|
+
const modules = new Set<string>();
|
|
60
|
+
|
|
61
|
+
for (const node of graph.nodes)
|
|
62
|
+
if (node.module && node.kind !== "component" && flowColumnOf.has(node.kind)) modules.add(node.module);
|
|
63
|
+
return [...modules].sort();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static erd(graph: AkanGraph, module: string | null): StudioScene {
|
|
67
|
+
const types = graph.nodes.filter((node) => typeKinds.has(node.kind));
|
|
68
|
+
const fieldEdges = graph.edges.filter((edge) => edge.kind === "field");
|
|
69
|
+
const { nodes, edges } = StudioLayout.#around(types, fieldEdges, module);
|
|
70
|
+
const ranks = StudioLayout.#ranks(nodes, edges);
|
|
71
|
+
const groups = [...new Set(ranks.values())]
|
|
72
|
+
.sort((a, b) => a - b)
|
|
73
|
+
.map((rank) => nodes.filter((node) => ranks.get(node.id) === rank));
|
|
74
|
+
return StudioLayout.#place(groups, edges, { width: 248, labels: null, rowsOf: StudioLayout.#typeRows });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static signals(graph: AkanGraph, module: string | null): StudioScene {
|
|
78
|
+
const flowNodes = graph.nodes.filter((node) => flowColumnOf.has(node.kind));
|
|
79
|
+
const flowEdges = graph.edges.filter((edge) => flowEdgeKinds.has(edge.kind));
|
|
80
|
+
const { nodes, edges } = StudioLayout.#around(flowNodes, flowEdges, module);
|
|
81
|
+
const groups = flowColumns.map((_, idx) => nodes.filter((node) => flowColumnOf.get(node.kind) === idx));
|
|
82
|
+
return StudioLayout.#place(groups, edges, {
|
|
83
|
+
width: 264,
|
|
84
|
+
labels: flowColumns.map(({ label }) => label),
|
|
85
|
+
rowsOf: StudioLayout.#chipRows,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static #around(nodes: AkanGraphNode[], edges: AkanGraphEdge[], module: string | null) {
|
|
90
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
91
|
+
const linked = edges.filter((edge) => byId.has(edge.from) && byId.has(edge.to));
|
|
92
|
+
if (!module) return { nodes, edges: linked };
|
|
93
|
+
const seeds = new Set(nodes.filter((node) => node.module === module).map((node) => node.id));
|
|
94
|
+
const kept = linked.filter((edge) => seeds.has(edge.from) || seeds.has(edge.to));
|
|
95
|
+
const ids = new Set([...seeds, ...kept.flatMap((edge) => [edge.from, edge.to])]);
|
|
96
|
+
return { nodes: nodes.filter((node) => ids.has(node.id)), edges: kept };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static #ranks(nodes: AkanGraphNode[], edges: AkanGraphEdge[]) {
|
|
100
|
+
const out = new Map<string, string[]>();
|
|
101
|
+
for (const edge of edges) out.set(edge.from, [...(out.get(edge.from) ?? []), edge.to]);
|
|
102
|
+
const state = new Map<string, "open" | "done">();
|
|
103
|
+
const order: string[] = [];
|
|
104
|
+
const visit = (id: string) => {
|
|
105
|
+
if (state.has(id)) return;
|
|
106
|
+
state.set(id, "open");
|
|
107
|
+
for (const to of out.get(id) ?? []) if (state.get(to) !== "open") visit(to);
|
|
108
|
+
state.set(id, "done");
|
|
109
|
+
order.push(id);
|
|
110
|
+
};
|
|
111
|
+
for (const node of nodes) visit(node.id);
|
|
112
|
+
order.reverse();
|
|
113
|
+
const position = new Map(order.map((id, idx) => [id, idx]));
|
|
114
|
+
const ranks = new Map(nodes.map((node) => [node.id, 0]));
|
|
115
|
+
for (const id of order)
|
|
116
|
+
for (const to of out.get(id) ?? [])
|
|
117
|
+
if ((position.get(to) ?? 0) > (position.get(id) ?? 0))
|
|
118
|
+
ranks.set(to, Math.max(ranks.get(to) ?? 0, (ranks.get(id) ?? 0) + 1));
|
|
119
|
+
return ranks;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static #typeRows(node: AkanGraphNode): Omit<StudioRow, "y">[] {
|
|
123
|
+
if (node.values) return node.values.map((value) => ({ text: value, hint: "", at: null }));
|
|
124
|
+
return (node.fields ?? []).map((field) => ({
|
|
125
|
+
text: field.name,
|
|
126
|
+
hint: `${field.type ?? "?"}${field.array ? "[]" : ""}${field.optional ? "?" : ""}`,
|
|
127
|
+
at: field.at,
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
static #chipRows(node: AkanGraphNode): Omit<StudioRow, "y">[] {
|
|
132
|
+
return node.detail.length ? [{ text: node.detail.join(" · "), hint: "", at: null }] : [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
static #place(
|
|
136
|
+
groups: AkanGraphNode[][],
|
|
137
|
+
edges: AkanGraphEdge[],
|
|
138
|
+
{
|
|
139
|
+
width,
|
|
140
|
+
labels,
|
|
141
|
+
rowsOf,
|
|
142
|
+
}: { width: number; labels: string[] | null; rowsOf: (node: AkanGraphNode) => Omit<StudioRow, "y">[] },
|
|
143
|
+
): StudioScene {
|
|
144
|
+
const ordered = StudioLayout.#order(groups, edges);
|
|
145
|
+
const boxes: StudioBox[] = [];
|
|
146
|
+
const columns: StudioColumn[] = [];
|
|
147
|
+
let x = padding;
|
|
148
|
+
let height = 0;
|
|
149
|
+
ordered.forEach((group, idx) => {
|
|
150
|
+
if (labels && !group.length) return;
|
|
151
|
+
columns.push({ label: labels?.[idx] ?? "", x });
|
|
152
|
+
let y = columnTop;
|
|
153
|
+
for (const node of group) {
|
|
154
|
+
const rows = rowsOf(node);
|
|
155
|
+
const boxHeight = headerHeight + rows.length * rowHeight + (rows.length ? 6 : 0);
|
|
156
|
+
if (y > columnTop && y + boxHeight > maxColumnHeight) {
|
|
157
|
+
x += width + boxGap * 2;
|
|
158
|
+
y = columnTop;
|
|
159
|
+
}
|
|
160
|
+
const placed = rows.map((row, rowIdx) => ({
|
|
161
|
+
...row,
|
|
162
|
+
y: y + headerHeight + rowIdx * rowHeight + rowHeight / 2,
|
|
163
|
+
}));
|
|
164
|
+
boxes.push({ node, x, y, width, height: boxHeight, rows: placed });
|
|
165
|
+
y += boxHeight + boxGap;
|
|
166
|
+
height = Math.max(height, y);
|
|
167
|
+
}
|
|
168
|
+
x += width + columnGap;
|
|
169
|
+
});
|
|
170
|
+
const boxOf = new Map(boxes.map((box) => [box.node.id, box]));
|
|
171
|
+
const links = edges.flatMap((edge) => {
|
|
172
|
+
const from = boxOf.get(edge.from);
|
|
173
|
+
const to = boxOf.get(edge.to);
|
|
174
|
+
return from && to ? [{ edge, path: StudioLayout.#path(from, to, edge) }] : [];
|
|
175
|
+
});
|
|
176
|
+
return { boxes, links, columns, width: x - columnGap + padding, height: height + padding };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
static #order(groups: AkanGraphNode[][], edges: AkanGraphEdge[]): AkanGraphNode[][] {
|
|
180
|
+
const byLabel = (a: AkanGraphNode, b: AkanGraphNode) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0);
|
|
181
|
+
const ordered = groups.map((group) => [...group].sort(byLabel));
|
|
182
|
+
const neighbors = new Map<string, string[]>();
|
|
183
|
+
for (const { from, to } of edges) {
|
|
184
|
+
neighbors.set(from, [...(neighbors.get(from) ?? []), to]);
|
|
185
|
+
neighbors.set(to, [...(neighbors.get(to) ?? []), from]);
|
|
186
|
+
}
|
|
187
|
+
const sweep = (indices: number[]) => {
|
|
188
|
+
const position = new Map(ordered.flatMap((group) => group.map((node, idx) => [node.id, idx] as const)));
|
|
189
|
+
for (const idx of indices) {
|
|
190
|
+
const center = (node: AkanGraphNode) => {
|
|
191
|
+
const around = (neighbors.get(node.id) ?? []).flatMap((id) => {
|
|
192
|
+
const at = position.get(id);
|
|
193
|
+
return at === undefined ? [] : [at];
|
|
194
|
+
});
|
|
195
|
+
return around.length ? around.reduce((sum, at) => sum + at, 0) / around.length : (position.get(node.id) ?? 0);
|
|
196
|
+
};
|
|
197
|
+
const centers = new Map(ordered[idx].map((node) => [node.id, center(node)]));
|
|
198
|
+
ordered[idx].sort((a, b) => (centers.get(a.id) ?? 0) - (centers.get(b.id) ?? 0));
|
|
199
|
+
for (const [at, node] of ordered[idx].entries()) position.set(node.id, at);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const forward = ordered.map((_, idx) => idx).slice(1);
|
|
203
|
+
sweep(forward);
|
|
204
|
+
sweep([...forward].reverse().map((idx) => idx - 1));
|
|
205
|
+
return ordered;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
static #path(from: StudioBox, to: StudioBox, edge: AkanGraphEdge): string {
|
|
209
|
+
const row =
|
|
210
|
+
edge.kind === "field"
|
|
211
|
+
? from.rows.find(({ text }) => edge.label === text || edge.label === `${text}[]`)
|
|
212
|
+
: undefined;
|
|
213
|
+
const y1 = row?.y ?? from.y + headerHeight / 2;
|
|
214
|
+
const y2 = to.y + headerHeight / 2;
|
|
215
|
+
const x1 = from.x + from.width;
|
|
216
|
+
if (to.x > from.x + from.width) {
|
|
217
|
+
const bend = Math.max(40, (to.x - x1) / 2);
|
|
218
|
+
return `M${x1},${y1} C${x1 + bend},${y1} ${to.x - bend},${y2} ${to.x},${y2}`;
|
|
219
|
+
}
|
|
220
|
+
const x2 = to.x + to.width;
|
|
221
|
+
const loop = Math.max(x1, x2) + 48;
|
|
222
|
+
return `M${x1},${y1} C${loop},${y1} ${loop},${y2} ${x2},${y2}`;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Logger } from "akanjs/common";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { BuildGraphPayload } from "../../artifact";
|
|
4
|
+
import { createSystemPageHeaders } from "../../systemPages";
|
|
5
|
+
import { StudioDocument, type StudioEditor, type StudioQuery } from "./studioDocument";
|
|
6
|
+
|
|
7
|
+
export interface StudioRouteContext {
|
|
8
|
+
basePath: string;
|
|
9
|
+
appName: string;
|
|
10
|
+
buildGraph: (() => Promise<BuildGraphPayload | null>) | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const editors = new Set<StudioEditor>(["vscode", "cursor"]);
|
|
14
|
+
|
|
15
|
+
export class StudioRoute {
|
|
16
|
+
static readonly logger = new Logger("StudioRoute");
|
|
17
|
+
readonly #context: StudioRouteContext;
|
|
18
|
+
|
|
19
|
+
constructor(context: StudioRouteContext) {
|
|
20
|
+
this.#context = context;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static queryOf(url: URL): StudioQuery {
|
|
24
|
+
const view = url.searchParams.get("view") === "signals" ? "signals" : "erd";
|
|
25
|
+
const editor = url.searchParams.get("editor");
|
|
26
|
+
return {
|
|
27
|
+
view,
|
|
28
|
+
module: url.searchParams.get("module") || null,
|
|
29
|
+
editor: editor && editors.has(editor as StudioEditor) ? (editor as StudioEditor) : "vscode",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async page(req: Request): Promise<Response> {
|
|
34
|
+
const { basePath, appName } = this.#context;
|
|
35
|
+
const loaded = await this.#load();
|
|
36
|
+
if ("error" in loaded) return await StudioRoute.#html(StudioDocument.unavailable(appName, loaded.error), 503);
|
|
37
|
+
const query = StudioRoute.queryOf(new URL(req.url));
|
|
38
|
+
return await StudioRoute.#html(StudioDocument.render({ basePath, appName, payload: loaded.payload, query }), 200);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async graph(): Promise<Response> {
|
|
42
|
+
const loaded = await this.#load();
|
|
43
|
+
if ("error" in loaded)
|
|
44
|
+
return Response.json({ error: loaded.error }, { status: 503, headers: { "Cache-Control": "no-store" } });
|
|
45
|
+
return Response.json(loaded.payload, { headers: { "Cache-Control": "no-store" } });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async #load(): Promise<{ payload: BuildGraphPayload } | { error: string }> {
|
|
49
|
+
const { buildGraph } = this.#context;
|
|
50
|
+
try {
|
|
51
|
+
const payload = buildGraph ? await buildGraph() : null;
|
|
52
|
+
return payload
|
|
53
|
+
? { payload }
|
|
54
|
+
: { error: "Akan Studio reads the graph from the dev builder; open it under `akan start`." };
|
|
55
|
+
} catch (error) {
|
|
56
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
57
|
+
StudioRoute.logger.warn(`Failed to build the Akan Graph: ${message}`);
|
|
58
|
+
return { error: message };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static #headers(): Headers {
|
|
63
|
+
const headers = createSystemPageHeaders();
|
|
64
|
+
headers.delete("X-Frame-Options");
|
|
65
|
+
headers.set("Content-Security-Policy", "frame-ancestors 'self' vscode-webview: vscode-file:; base-uri 'self'");
|
|
66
|
+
return headers;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static async #html(document: ReactNode, status: number): Promise<Response> {
|
|
70
|
+
const { renderToReadableStream } = await import("react-dom/server.browser");
|
|
71
|
+
return new Response(await renderToReadableStream(document), { status, headers: StudioRoute.#headers() });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const studioRuntime = () => {
|
|
2
|
+
if (window.parent !== window) {
|
|
3
|
+
let host: string | null = null;
|
|
4
|
+
window.addEventListener("message", (event) => {
|
|
5
|
+
if (event.source !== window.parent || !event.origin.startsWith("vscode-webview:")) return;
|
|
6
|
+
if ((event.data as { type?: unknown } | null)?.type !== "akan-studio:host") return;
|
|
7
|
+
host = event.origin;
|
|
8
|
+
window.parent.postMessage({ type: "akan-studio:ready", href: location.href }, host);
|
|
9
|
+
});
|
|
10
|
+
document.addEventListener("click", (event) => {
|
|
11
|
+
const href = event.target instanceof Element ? event.target.closest("a[href]")?.getAttribute("href") : null;
|
|
12
|
+
const source = href?.match(/^[a-z-]+:\/\/file(\/.+):(\d+):(\d+)$/);
|
|
13
|
+
if (!host || !source) return;
|
|
14
|
+
event.preventDefault();
|
|
15
|
+
const [, path, line, column] = source;
|
|
16
|
+
window.parent.postMessage(
|
|
17
|
+
{ type: "akan-studio:open", path: decodeURI(path), line: +line, column: +column },
|
|
18
|
+
host,
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const select = document.querySelector<HTMLSelectElement>("select[data-studio-module]");
|
|
23
|
+
select?.addEventListener("change", () => select.form?.submit());
|
|
24
|
+
const svg = document.querySelector<SVGSVGElement>("svg[data-studio]");
|
|
25
|
+
if (!svg) return;
|
|
26
|
+
const links = [...svg.querySelectorAll<SVGPathElement>("path[data-from]")];
|
|
27
|
+
const boxes = new Map(
|
|
28
|
+
[...svg.querySelectorAll<SVGGElement>("g[data-node]")].map((box) => [box.dataset.node ?? "", box] as const),
|
|
29
|
+
);
|
|
30
|
+
let pinned: string | null = null;
|
|
31
|
+
const focus = (id: string | null) => {
|
|
32
|
+
svg.classList.toggle("focused", id !== null);
|
|
33
|
+
const near = new Set(id ? [id] : []);
|
|
34
|
+
for (const link of links) {
|
|
35
|
+
const isHot = id !== null && (link.dataset.from === id || link.dataset.to === id);
|
|
36
|
+
link.classList.toggle("hot", isHot);
|
|
37
|
+
if (isHot) for (const end of [link.dataset.from, link.dataset.to]) if (end) near.add(end);
|
|
38
|
+
}
|
|
39
|
+
for (const [key, box] of boxes) box.classList.toggle("hot", near.has(key));
|
|
40
|
+
};
|
|
41
|
+
for (const [id, box] of boxes) {
|
|
42
|
+
box.addEventListener("mouseenter", () => {
|
|
43
|
+
if (!pinned) focus(id);
|
|
44
|
+
});
|
|
45
|
+
box.addEventListener("mouseleave", () => {
|
|
46
|
+
if (!pinned) focus(null);
|
|
47
|
+
});
|
|
48
|
+
box.addEventListener("click", (event) => {
|
|
49
|
+
if (event.target instanceof Element && event.target.closest("a[href]")) return;
|
|
50
|
+
pinned = pinned === id ? null : id;
|
|
51
|
+
focus(pinned ?? id);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
document.addEventListener("keydown", (event) => {
|
|
55
|
+
if (event.key !== "Escape") return;
|
|
56
|
+
pinned = null;
|
|
57
|
+
focus(null);
|
|
58
|
+
});
|
|
59
|
+
const filter = document.querySelector<HTMLInputElement>("input[data-studio-filter]");
|
|
60
|
+
filter?.addEventListener("input", () => {
|
|
61
|
+
const query = filter.value.trim().toLowerCase();
|
|
62
|
+
for (const box of boxes.values())
|
|
63
|
+
box.classList.toggle("muted", query !== "" && !(box.dataset.label ?? "").toLowerCase().includes(query));
|
|
64
|
+
});
|
|
65
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const studioStyle = `
|
|
2
|
+
:root { color-scheme: light dark; --bg: #f6f6f7; --fg: #1d1d1f; --muted: #6e6e73; --panel: #ffffff; --line: #c9c9ce;
|
|
3
|
+
--model: #2563eb; --scalar: #0d9488; --enum: #9333ea; --query: #64748b; --slice: #ea580c; --endpoint: #dc2626;
|
|
4
|
+
--method: #0891b2; --action: #16a34a; --component: #ca8a04; }
|
|
5
|
+
@media (prefers-color-scheme: dark) {
|
|
6
|
+
:root { --bg: #111113; --fg: #ececf1; --muted: #9a9aa3; --panel: #1c1c1f; --line: #3d3d44; }
|
|
7
|
+
}
|
|
8
|
+
body { margin: 0; font: 13px/1.4 ui-sans-serif, system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--fg); }
|
|
9
|
+
header { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
|
|
10
|
+
padding: 10px 16px; background: var(--panel); border-bottom: 1px solid var(--line); }
|
|
11
|
+
header nav { display: flex; gap: 4px; }
|
|
12
|
+
header nav.end { margin-left: auto; }
|
|
13
|
+
header a { color: inherit; text-decoration: none; padding: 4px 8px; border-radius: 6px; }
|
|
14
|
+
header a[aria-current] { background: var(--line); }
|
|
15
|
+
header select, header input { font: inherit; color: inherit; background: var(--bg); border: 1px solid var(--line);
|
|
16
|
+
border-radius: 6px; padding: 4px 8px; }
|
|
17
|
+
.muted { color: var(--muted); }
|
|
18
|
+
.empty { padding: 48px; color: var(--muted); }
|
|
19
|
+
main { overflow: auto; }
|
|
20
|
+
svg { display: block; }
|
|
21
|
+
svg text { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; fill: var(--fg); }
|
|
22
|
+
svg a { cursor: pointer; }
|
|
23
|
+
svg a:hover text { text-decoration: underline; }
|
|
24
|
+
.column { fill: var(--muted); font-weight: 600; letter-spacing: 0.04em; }
|
|
25
|
+
.box { cursor: default; }
|
|
26
|
+
.box rect { fill: var(--panel); stroke: var(--line); }
|
|
27
|
+
.box rect.bar { fill: currentColor; stroke: none; }
|
|
28
|
+
.box .title { font-weight: 600; }
|
|
29
|
+
.box .kind, .box .hint { fill: var(--muted); }
|
|
30
|
+
.box.model { color: var(--model); } .box.scalar { color: var(--scalar); } .box.enum { color: var(--enum); }
|
|
31
|
+
.box.query { color: var(--query); } .box.slice { color: var(--slice); } .box.endpoint { color: var(--endpoint); }
|
|
32
|
+
.box.method { color: var(--method); } .box.action { color: var(--action); } .box.component { color: var(--component); }
|
|
33
|
+
.link { fill: none; stroke: var(--line); stroke-width: 1.25; }
|
|
34
|
+
.link.fetches { stroke-dasharray: 5 3; }
|
|
35
|
+
.link.uses { stroke-dasharray: 2 3; }
|
|
36
|
+
.arrow { fill: var(--muted); }
|
|
37
|
+
svg.focused .box:not(.hot), svg.focused .link:not(.hot), .box.muted { opacity: 0.16; }
|
|
38
|
+
.link.hot { stroke: var(--fg); stroke-width: 2; }
|
|
39
|
+
`;
|
package/server/webRouter.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type { PagePromptSource } from "../signal/mcp/pagePrompt";
|
|
|
23
23
|
import { SignalContext } from "../signal/signalContext";
|
|
24
24
|
import {
|
|
25
25
|
type BuilderRpc,
|
|
26
|
+
type BuildGraphPayload,
|
|
26
27
|
type MergedManifest,
|
|
27
28
|
RouteClientCache,
|
|
28
29
|
type RouteSeedIndex,
|
|
@@ -808,6 +809,9 @@ export class WebRouter {
|
|
|
808
809
|
};
|
|
809
810
|
return { renderEnvRoutes, hmrHub: this.#hub, builderRpc: this.#builderRpc };
|
|
810
811
|
}
|
|
812
|
+
async buildGraph(): Promise<BuildGraphPayload | null> {
|
|
813
|
+
return this.#builderRpc ? await this.#builderRpc.buildGraph() : null;
|
|
814
|
+
}
|
|
811
815
|
refreshHmrState(): void {
|
|
812
816
|
this.#devHmr?.refreshRegistryState();
|
|
813
817
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type AkanGraphNodeKind = "model" | "scalar" | "enum" | "query" | "slice" | "endpoint" | "guard" | "method" | "action" | "component";
|
|
2
|
+
export type AkanGraphEdgeKind = "field" | "guard" | "calls" | "fetches" | "uses";
|
|
3
|
+
export interface AkanGraphLocation {
|
|
4
|
+
file: string;
|
|
5
|
+
line: number;
|
|
6
|
+
column: number;
|
|
7
|
+
}
|
|
8
|
+
export interface AkanGraphField {
|
|
9
|
+
name: string;
|
|
10
|
+
type: string | null;
|
|
11
|
+
array: boolean;
|
|
12
|
+
optional: boolean;
|
|
13
|
+
ref: string | null;
|
|
14
|
+
declaredIn: string;
|
|
15
|
+
at: AkanGraphLocation;
|
|
16
|
+
}
|
|
17
|
+
export interface AkanGraphNode {
|
|
18
|
+
id: string;
|
|
19
|
+
kind: AkanGraphNodeKind;
|
|
20
|
+
name: string;
|
|
21
|
+
label: string;
|
|
22
|
+
owner: string | null;
|
|
23
|
+
module: string | null;
|
|
24
|
+
detail: string[];
|
|
25
|
+
at: AkanGraphLocation | null;
|
|
26
|
+
fields?: AkanGraphField[];
|
|
27
|
+
values?: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface AkanGraphEdge {
|
|
30
|
+
from: string;
|
|
31
|
+
to: string;
|
|
32
|
+
kind: AkanGraphEdgeKind;
|
|
33
|
+
label: string | null;
|
|
34
|
+
count: number;
|
|
35
|
+
at: AkanGraphLocation | null;
|
|
36
|
+
}
|
|
37
|
+
export interface AkanGraph {
|
|
38
|
+
version: 1;
|
|
39
|
+
nodes: AkanGraphNode[];
|
|
40
|
+
edges: AkanGraphEdge[];
|
|
41
|
+
}
|
package/types/common/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type { AkanGraph, AkanGraphEdge, AkanGraphEdgeKind, AkanGraphField, AkanGraphLocation, AkanGraphNode, AkanGraphNodeKind, } from "./akanGraph.d.ts";
|
|
1
2
|
export { databaseStateNames, endpointNames, formSetterNames, modelNames, moduleNames, queryNames, sliceNames, stateSetterName, } from "./akanNames.d.ts";
|
|
2
3
|
export { AppHosts } from "./appHosts.d.ts";
|
|
3
4
|
export { applyMixins, getAllPropertyDescriptors } from "./applyMixins.d.ts";
|
|
@@ -8,7 +8,7 @@ export declare class ConstantRegistry {
|
|
|
8
8
|
static enum: Map<string, EnumInstance>;
|
|
9
9
|
static value: Map<string, unknown>;
|
|
10
10
|
static modelRefNameMap: Map<new (...args: never[]) => unknown, string>;
|
|
11
|
-
static origin: Map<`
|
|
11
|
+
static origin: Map<`scalar:${string}` | `enum:${string}` | `database:${string}`, string[]>;
|
|
12
12
|
static getOrigin(kind: ConstantOriginKind, key: string): string[];
|
|
13
13
|
static assertConsistentRefNames(skippedOrigins?: readonly string[]): void;
|
|
14
14
|
static has(modelRef: Cls): boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuildRouteClientResult, CsrUpdatedPayload, CssPayload, DevBuildStatus, PagesBundlePayload, SsrUpdatedPayload } from "./ipcTypes.d.ts";
|
|
1
|
+
import type { BuildGraphPayload, BuildRouteClientResult, CsrUpdatedPayload, CssPayload, DevBuildStatus, PagesBundlePayload, SsrUpdatedPayload } from "./ipcTypes.d.ts";
|
|
2
2
|
export interface BuilderRpcEventHandlers {
|
|
3
3
|
/** A watcher batch changed files: clear the route cache and broadcast a dev reload. */
|
|
4
4
|
onInvalidate?: (event: {
|
|
@@ -30,5 +30,6 @@ export declare class BuilderRpc {
|
|
|
30
30
|
buildCsr(reason: string, { shell }?: {
|
|
31
31
|
shell?: boolean;
|
|
32
32
|
}): Promise<void>;
|
|
33
|
+
buildGraph(): Promise<BuildGraphPayload>;
|
|
33
34
|
dispose(): void;
|
|
34
35
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AkanGraph } from "akanjs/common";
|
|
1
2
|
import type { SsrManifest } from "../ssrTypes.d.ts";
|
|
2
3
|
export interface BuildRouteResultPayload {
|
|
3
4
|
manifestDelta: Record<string, {
|
|
@@ -93,6 +94,25 @@ export type BuilderCsrRes = {
|
|
|
93
94
|
ok: false;
|
|
94
95
|
error: string;
|
|
95
96
|
};
|
|
97
|
+
export interface BuildGraphPayload {
|
|
98
|
+
workspaceRoot: string;
|
|
99
|
+
graph: AkanGraph;
|
|
100
|
+
}
|
|
101
|
+
export type BuilderGraphReq = {
|
|
102
|
+
type: "build-graph";
|
|
103
|
+
id: number;
|
|
104
|
+
};
|
|
105
|
+
export type BuilderGraphRes = {
|
|
106
|
+
type: "build-graph-res";
|
|
107
|
+
id: number;
|
|
108
|
+
ok: true;
|
|
109
|
+
data: BuildGraphPayload;
|
|
110
|
+
} | {
|
|
111
|
+
type: "build-graph-res";
|
|
112
|
+
id: number;
|
|
113
|
+
ok: false;
|
|
114
|
+
error: string;
|
|
115
|
+
};
|
|
96
116
|
/** Drain and exit (the only way bundler memory returns to the OS); draining, not killing, keeps a rebuild whole. */
|
|
97
117
|
export type BuilderControl = {
|
|
98
118
|
type: "builder-shutdown";
|
|
@@ -189,7 +209,9 @@ export type BuilderEvent = {
|
|
|
189
209
|
| {
|
|
190
210
|
type: "boot-armed";
|
|
191
211
|
};
|
|
192
|
-
export type
|
|
212
|
+
export type BuilderRequest = BuilderReq | BuilderCsrReq | BuilderGraphReq;
|
|
213
|
+
export type BuilderResponse = BuilderRes | BuilderCsrRes | BuilderGraphRes;
|
|
214
|
+
export type BuilderMessage = BuilderRequest | BuilderResponse | BuilderControl | BuilderEvent;
|
|
193
215
|
export interface ClientManifestEntry {
|
|
194
216
|
id: string;
|
|
195
217
|
chunks: string[];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BaseEnv } from "akanjs/base";
|
|
2
2
|
import { Logger } from "akanjs/common";
|
|
3
|
+
import type { BuildGraphPayload } from "../artifact.d.ts";
|
|
3
4
|
import type { DiLifecycle } from "../di/diLifecycle.d.ts";
|
|
4
5
|
import type { HttpRoutes } from "../types.d.ts";
|
|
5
6
|
export interface DevtoolsRouterContext {
|
|
@@ -11,6 +12,7 @@ export interface DevtoolsRouterContext {
|
|
|
11
12
|
websocketPrefix: string;
|
|
12
13
|
openapi: boolean;
|
|
13
14
|
getStatus: () => string;
|
|
15
|
+
buildGraph?: () => Promise<BuildGraphPayload | null>;
|
|
14
16
|
}
|
|
15
17
|
/** Builtin routes, not signals: off `/api` and guards, and never in the `serializedSignal` payload sent to clients. */
|
|
16
18
|
export declare class DevtoolsRouter {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AkanGraphLocation } from "akanjs/common";
|
|
2
|
+
import type { BuildGraphPayload } from "../../artifact.d.ts";
|
|
3
|
+
import { type StudioView } from "./studioLayout.d.ts";
|
|
4
|
+
export type StudioEditor = "vscode" | "cursor";
|
|
5
|
+
export interface StudioQuery {
|
|
6
|
+
view: StudioView;
|
|
7
|
+
module: string | null;
|
|
8
|
+
editor: StudioEditor;
|
|
9
|
+
}
|
|
10
|
+
export interface StudioDocumentProps {
|
|
11
|
+
basePath: string;
|
|
12
|
+
appName: string;
|
|
13
|
+
payload: BuildGraphPayload;
|
|
14
|
+
query: StudioQuery;
|
|
15
|
+
}
|
|
16
|
+
export declare class StudioDocument {
|
|
17
|
+
#private;
|
|
18
|
+
static render({ basePath, appName, payload, query }: StudioDocumentProps): import("react").JSX.Element;
|
|
19
|
+
static unavailable(appName: string, reason: string): import("react").JSX.Element;
|
|
20
|
+
static href(basePath: string, { view, module, editor }: StudioQuery): string;
|
|
21
|
+
static editorHref(editor: StudioEditor, workspaceRoot: string, at: AkanGraphLocation): string;
|
|
22
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AkanGraph, AkanGraphEdge, AkanGraphLocation, AkanGraphNode } from "akanjs/common";
|
|
2
|
+
export type StudioView = "erd" | "signals";
|
|
3
|
+
export interface StudioRow {
|
|
4
|
+
text: string;
|
|
5
|
+
hint: string;
|
|
6
|
+
y: number;
|
|
7
|
+
at: AkanGraphLocation | null;
|
|
8
|
+
}
|
|
9
|
+
export interface StudioBox {
|
|
10
|
+
node: AkanGraphNode;
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
rows: StudioRow[];
|
|
16
|
+
}
|
|
17
|
+
export interface StudioLink {
|
|
18
|
+
edge: AkanGraphEdge;
|
|
19
|
+
path: string;
|
|
20
|
+
}
|
|
21
|
+
export interface StudioColumn {
|
|
22
|
+
label: string;
|
|
23
|
+
x: number;
|
|
24
|
+
}
|
|
25
|
+
export interface StudioScene {
|
|
26
|
+
boxes: StudioBox[];
|
|
27
|
+
links: StudioLink[];
|
|
28
|
+
columns: StudioColumn[];
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
}
|
|
32
|
+
export declare class StudioLayout {
|
|
33
|
+
#private;
|
|
34
|
+
static modulesOf(graph: AkanGraph): string[];
|
|
35
|
+
static erd(graph: AkanGraph, module: string | null): StudioScene;
|
|
36
|
+
static signals(graph: AkanGraph, module: string | null): StudioScene;
|
|
37
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Logger } from "akanjs/common";
|
|
2
|
+
import type { BuildGraphPayload } from "../../artifact.d.ts";
|
|
3
|
+
import { type StudioQuery } from "./studioDocument.d.ts";
|
|
4
|
+
export interface StudioRouteContext {
|
|
5
|
+
basePath: string;
|
|
6
|
+
appName: string;
|
|
7
|
+
buildGraph: (() => Promise<BuildGraphPayload | null>) | null;
|
|
8
|
+
}
|
|
9
|
+
export declare class StudioRoute {
|
|
10
|
+
#private;
|
|
11
|
+
static readonly logger: Logger;
|
|
12
|
+
constructor(context: StudioRouteContext);
|
|
13
|
+
static queryOf(url: URL): StudioQuery;
|
|
14
|
+
page(req: Request): Promise<Response>;
|
|
15
|
+
graph(): Promise<Response>;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const studioRuntime: () => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const studioStyle = "\n:root { color-scheme: light dark; --bg: #f6f6f7; --fg: #1d1d1f; --muted: #6e6e73; --panel: #ffffff; --line: #c9c9ce;\n --model: #2563eb; --scalar: #0d9488; --enum: #9333ea; --query: #64748b; --slice: #ea580c; --endpoint: #dc2626;\n --method: #0891b2; --action: #16a34a; --component: #ca8a04; }\n@media (prefers-color-scheme: dark) {\n :root { --bg: #111113; --fg: #ececf1; --muted: #9a9aa3; --panel: #1c1c1f; --line: #3d3d44; }\n}\nbody { margin: 0; font: 13px/1.4 ui-sans-serif, system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--fg); }\nheader { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; gap: 12px; align-items: center;\n padding: 10px 16px; background: var(--panel); border-bottom: 1px solid var(--line); }\nheader nav { display: flex; gap: 4px; }\nheader nav.end { margin-left: auto; }\nheader a { color: inherit; text-decoration: none; padding: 4px 8px; border-radius: 6px; }\nheader a[aria-current] { background: var(--line); }\nheader select, header input { font: inherit; color: inherit; background: var(--bg); border: 1px solid var(--line);\n border-radius: 6px; padding: 4px 8px; }\n.muted { color: var(--muted); }\n.empty { padding: 48px; color: var(--muted); }\nmain { overflow: auto; }\nsvg { display: block; }\nsvg text { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; fill: var(--fg); }\nsvg a { cursor: pointer; }\nsvg a:hover text { text-decoration: underline; }\n.column { fill: var(--muted); font-weight: 600; letter-spacing: 0.04em; }\n.box { cursor: default; }\n.box rect { fill: var(--panel); stroke: var(--line); }\n.box rect.bar { fill: currentColor; stroke: none; }\n.box .title { font-weight: 600; }\n.box .kind, .box .hint { fill: var(--muted); }\n.box.model { color: var(--model); } .box.scalar { color: var(--scalar); } .box.enum { color: var(--enum); }\n.box.query { color: var(--query); } .box.slice { color: var(--slice); } .box.endpoint { color: var(--endpoint); }\n.box.method { color: var(--method); } .box.action { color: var(--action); } .box.component { color: var(--component); }\n.link { fill: none; stroke: var(--line); stroke-width: 1.25; }\n.link.fetches { stroke-dasharray: 5 3; }\n.link.uses { stroke-dasharray: 2 3; }\n.arrow { fill: var(--muted); }\nsvg.focused .box:not(.hot), svg.focused .link:not(.hot), .box.muted { opacity: 0.16; }\n.link.hot { stroke: var(--fg); stroke-width: 2; }\n";
|
|
@@ -27,7 +27,7 @@ export declare class MigrationCommand {
|
|
|
27
27
|
static parse(argv: string[], invokedAs?: string): MigrationArgs;
|
|
28
28
|
static run(runner: MigrationRunner, args: MigrationArgs): Promise<MigrationCommandResult>;
|
|
29
29
|
static failure(action: MigrationAction | null, error: unknown): MigrationCommandResult;
|
|
30
|
-
static exitCode(result: MigrationCommandResult):
|
|
31
|
-
static print(result: MigrationCommandResult, json: boolean): Promise<
|
|
30
|
+
static exitCode(result: MigrationCommandResult): 1 | 0 | 2 | 3;
|
|
31
|
+
static print(result: MigrationCommandResult, json: boolean): Promise<1 | 0 | 2 | 3>;
|
|
32
32
|
static render(result: MigrationCommandResult, json: boolean): string;
|
|
33
33
|
}
|
|
@@ -8,5 +8,5 @@ export declare class Migrations {
|
|
|
8
8
|
/** On a started server: the runner writes through its database adaptor. */
|
|
9
9
|
runner(server: AkanServer): MigrationRunner;
|
|
10
10
|
/** Boots `server` with no listener or schedule, runs one action, and prints its result last. Returns the exit code. */
|
|
11
|
-
command(server: AkanServer, argv: string[], invokedAs?: string): Promise<
|
|
11
|
+
command(server: AkanServer, argv: string[], invokedAs?: string): Promise<1 | 0 | 2 | 3>;
|
|
12
12
|
}
|
|
@@ -3,7 +3,7 @@ import { type AkanI18nConfig, type LogRecord } from "akanjs/common";
|
|
|
3
3
|
import { type AkanRequestStore } from "akanjs/fetch";
|
|
4
4
|
import type { AkanMetricsReport } from "akanjs/service";
|
|
5
5
|
import type { PagePromptSource } from "../signal/mcp/pagePrompt.d.ts";
|
|
6
|
-
import { type BuilderRpc, type RouteSeedIndex, type RoutesManifest } from "./artifact.d.ts";
|
|
6
|
+
import { type BuilderRpc, type BuildGraphPayload, type RouteSeedIndex, type RoutesManifest } from "./artifact.d.ts";
|
|
7
7
|
import { type RouteCacheInvalidation, type RouteCacheRenderState } from "./cachePolicy.d.ts";
|
|
8
8
|
import type { HmrWsData, HmrWsHub } from "./hmr/wsHub.d.ts";
|
|
9
9
|
import type { McpAppShellSource } from "./mcp/McpApps.d.ts";
|
|
@@ -66,6 +66,7 @@ export declare class WebRouter {
|
|
|
66
66
|
hmrHub: HmrWsHub | null;
|
|
67
67
|
builderRpc: BuilderRpc | null;
|
|
68
68
|
}>;
|
|
69
|
+
buildGraph(): Promise<BuildGraphPayload | null>;
|
|
69
70
|
refreshHmrState(): void;
|
|
70
71
|
hmrBuildErrors(): {
|
|
71
72
|
phase: string;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/** 입력 표면 look — Input/TextArea/DatePicker 가 공유하는 필드 셸. kind 로 한 줄 필드(field)/멀티라인(area)/네이티브 `<select>`(select), tone 으로 강조/오류 상태를 고른다. */
|
|
2
2
|
export declare const inputRecipe: (variants?: Omit<({
|
|
3
|
-
kind?: "
|
|
3
|
+
kind?: "field" | "select" | "area" | undefined;
|
|
4
4
|
size?: "xl" | "lg" | "md" | "sm" | "xs" | undefined;
|
|
5
5
|
tone?: "default" | "error" | "primary" | undefined;
|
|
6
6
|
} & {
|
|
7
7
|
class?: import("tailwind-variants/lite").ClassValue;
|
|
8
8
|
className?: never;
|
|
9
9
|
}) | ({
|
|
10
|
-
kind?: "
|
|
10
|
+
kind?: "field" | "select" | "area" | undefined;
|
|
11
11
|
size?: "xl" | "lg" | "md" | "sm" | "xs" | undefined;
|
|
12
12
|
tone?: "default" | "error" | "primary" | undefined;
|
|
13
13
|
} & {
|