@akanjs/language 3.1.2-rc.3 → 3.1.2-rc.4
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/README.ko.md +41 -0
- package/README.md +42 -0
- package/akanGraph.d.ts +1 -0
- package/akanGraphDiff.d.ts +10 -0
- package/akanIndex.d.ts +3 -1
- package/index.cjs +122 -17
- package/index.d.ts +2 -1
- package/index.js +122 -17
- package/moduleFacts.d.ts +2 -1
- package/package.json +4 -1
- package/server.js +1890 -0
package/README.ko.md
CHANGED
|
@@ -40,3 +40,44 @@ index.renameAt(fileName, offset, "inFeed", readText);
|
|
|
40
40
|
키에서 만들어진 이름 전부와 모듈 dictionary의 키와 함께 바꾸는 편집을 돌려줍니다.
|
|
41
41
|
|
|
42
42
|
ES module과 CommonJS로 배포하며, 런타임 의존성은 없습니다.
|
|
43
|
+
|
|
44
|
+
## 언어 서버
|
|
45
|
+
|
|
46
|
+
`akan-language-server --stdio`는 같은 색인을 LSP로 냅니다. Akan 확장이 있는 VS Code와 Cursor 말고 다른 에디터를
|
|
47
|
+
위한 것입니다. TypeScript 서버 옆에 띄우면 에디터가 두 서버의 답을 합쳐 줍니다. 정의, hover, 참조를 답합니다.
|
|
48
|
+
에디터의 루트나 그 위에서 가장 가까운, `apps/*`나 `libs/*`에 `akan.config.ts`가 있는 폴더를 색인합니다. 에디터가
|
|
49
|
+
watched-file 등록을 지원하면 디스크에서 바뀐 파일도 다시 읽습니다.
|
|
50
|
+
|
|
51
|
+
설치 절처럼 워크스페이스에 추가해서 bin이 `node_modules/.bin`에 들어가게 하세요. 그러면 `bunx --no-install`이 그
|
|
52
|
+
사본을 실행하고, 레지스트리에서 받아 오지 않습니다.
|
|
53
|
+
|
|
54
|
+
Neovim 0.11 이상:
|
|
55
|
+
|
|
56
|
+
```lua
|
|
57
|
+
vim.lsp.config("akan", {
|
|
58
|
+
cmd = function(dispatchers, config)
|
|
59
|
+
return vim.lsp.rpc.start({ "bunx", "--no-install", "akan-language-server", "--stdio" }, dispatchers, {
|
|
60
|
+
cwd = config.root_dir,
|
|
61
|
+
})
|
|
62
|
+
end,
|
|
63
|
+
filetypes = { "typescript", "typescriptreact" },
|
|
64
|
+
root_markers = { "bun.lock" },
|
|
65
|
+
})
|
|
66
|
+
vim.lsp.enable("akan")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Helix의 `languages.toml`:
|
|
70
|
+
|
|
71
|
+
```toml
|
|
72
|
+
[language-server.akan]
|
|
73
|
+
command = "bunx"
|
|
74
|
+
args = ["--no-install", "akan-language-server", "--stdio"]
|
|
75
|
+
|
|
76
|
+
[[language]]
|
|
77
|
+
name = "typescript"
|
|
78
|
+
language-servers = ["typescript-language-server", "akan"]
|
|
79
|
+
|
|
80
|
+
[[language]]
|
|
81
|
+
name = "tsx"
|
|
82
|
+
language-servers = ["typescript-language-server", "akan"]
|
|
83
|
+
```
|
package/README.md
CHANGED
|
@@ -40,3 +40,45 @@ name resolves within its own app and every lib, never another app. `renameAt` re
|
|
|
40
40
|
query key together with every name derived from it and its key in the module dictionary.
|
|
41
41
|
|
|
42
42
|
The package ships as an ES module and as CommonJS, with no runtime dependency.
|
|
43
|
+
|
|
44
|
+
## Language server
|
|
45
|
+
|
|
46
|
+
`akan-language-server --stdio` serves the same index over LSP. It is for editors other than VS Code and Cursor,
|
|
47
|
+
which have the Akan extension. Run it next to TypeScript's server, and the editor merges the two servers' answers. It
|
|
48
|
+
answers definition, hover and references. It indexes the nearest folder at or above the editor's root whose `apps/*`
|
|
49
|
+
or `libs/*` holds an `akan.config.ts`. It rereads files changed on disk when the editor supports watched-file
|
|
50
|
+
registration.
|
|
51
|
+
|
|
52
|
+
Add it to the workspace, as in Install, so the bin lands in `node_modules/.bin`. `bunx --no-install` then runs that
|
|
53
|
+
copy and never fetches one from the registry.
|
|
54
|
+
|
|
55
|
+
Neovim 0.11 or later:
|
|
56
|
+
|
|
57
|
+
```lua
|
|
58
|
+
vim.lsp.config("akan", {
|
|
59
|
+
cmd = function(dispatchers, config)
|
|
60
|
+
return vim.lsp.rpc.start({ "bunx", "--no-install", "akan-language-server", "--stdio" }, dispatchers, {
|
|
61
|
+
cwd = config.root_dir,
|
|
62
|
+
})
|
|
63
|
+
end,
|
|
64
|
+
filetypes = { "typescript", "typescriptreact" },
|
|
65
|
+
root_markers = { "bun.lock" },
|
|
66
|
+
})
|
|
67
|
+
vim.lsp.enable("akan")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Helix, in `languages.toml`:
|
|
71
|
+
|
|
72
|
+
```toml
|
|
73
|
+
[language-server.akan]
|
|
74
|
+
command = "bunx"
|
|
75
|
+
args = ["--no-install", "akan-language-server", "--stdio"]
|
|
76
|
+
|
|
77
|
+
[[language]]
|
|
78
|
+
name = "typescript"
|
|
79
|
+
language-servers = ["typescript-language-server", "akan"]
|
|
80
|
+
|
|
81
|
+
[[language]]
|
|
82
|
+
name = "tsx"
|
|
83
|
+
language-servers = ["typescript-language-server", "akan"]
|
|
84
|
+
```
|
package/akanGraph.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface AkanGraphSource {
|
|
|
12
12
|
resolve: (fileName: string, identifier: TS.Identifier) => AkanTarget[];
|
|
13
13
|
relative: (fileName: string) => string;
|
|
14
14
|
ownerOf: (fileName: string) => string | null;
|
|
15
|
+
digests?: boolean;
|
|
15
16
|
}
|
|
16
17
|
export declare class GraphBuilder {
|
|
17
18
|
#private;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AkanGraph, AkanGraphChange } from "akanjs/common/akanGraph.ts";
|
|
2
|
+
export interface AkanGraphChanges extends AkanGraph {
|
|
3
|
+
summary: {
|
|
4
|
+
[change in AkanGraphChange]: number;
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export declare class AkanGraphDiff {
|
|
8
|
+
#private;
|
|
9
|
+
static between(base: AkanGraph, head: AkanGraph): AkanGraphChanges;
|
|
10
|
+
}
|
package/akanIndex.d.ts
CHANGED
|
@@ -33,7 +33,9 @@ export declare class AkanIndex {
|
|
|
33
33
|
sourceKeysOf(givenName: string): AkanSourceKey[];
|
|
34
34
|
sourceKeyAt(givenName: string, offset: number): AkanSourceKey | null;
|
|
35
35
|
referencesAt(givenName: string, offset: number, readText: (fileName: string) => string | undefined): AkanSpan[];
|
|
36
|
-
graph(readText: (fileName: string) => string | undefined
|
|
36
|
+
graph(readText: (fileName: string) => string | undefined, { digests }?: {
|
|
37
|
+
digests?: boolean | undefined;
|
|
38
|
+
}): AkanGraph;
|
|
37
39
|
renameTargetAt(givenName: string, offset: number): {
|
|
38
40
|
family: AkanFamily;
|
|
39
41
|
sourceKey: string;
|
package/index.cjs
CHANGED
|
@@ -39,6 +39,7 @@ var __export = (target, all) => {
|
|
|
39
39
|
// pkgs/@akanjs/language/index.ts
|
|
40
40
|
var exports_language = {};
|
|
41
41
|
__export(exports_language, {
|
|
42
|
+
AkanGraphDiff: () => AkanGraphDiff,
|
|
42
43
|
AkanIndex: () => AkanIndex,
|
|
43
44
|
createAst: () => createAst,
|
|
44
45
|
createFactReader: () => createFactReader,
|
|
@@ -46,6 +47,79 @@ __export(exports_language, {
|
|
|
46
47
|
});
|
|
47
48
|
module.exports = __toCommonJS(exports_language);
|
|
48
49
|
|
|
50
|
+
// pkgs/@akanjs/language/akanGraphDiff.ts
|
|
51
|
+
var edgeKeyOf = ({ from, to, kind, label }) => `${from}
|
|
52
|
+
${to}
|
|
53
|
+
${kind}
|
|
54
|
+
${label ?? ""}`;
|
|
55
|
+
var shapeOf = ({ type, array, optional, declaredIn }) => `${type ?? ""}${array ? "[]" : ""}${optional ? "?" : ""} ${declaredIn}`;
|
|
56
|
+
var isEdited = (before, after) => before.digest !== undefined && after.digest !== undefined && before.digest !== after.digest;
|
|
57
|
+
var byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
58
|
+
|
|
59
|
+
class AkanGraphDiff {
|
|
60
|
+
static between(base, head) {
|
|
61
|
+
const baseNodes = new Map(base.nodes.map((node) => [node.id, node]));
|
|
62
|
+
const headNodes = new Map(head.nodes.map((node) => [node.id, node]));
|
|
63
|
+
const marked = new Map;
|
|
64
|
+
for (const node of head.nodes) {
|
|
65
|
+
const before = baseNodes.get(node.id);
|
|
66
|
+
const next = before ? AkanGraphDiff.#compare(before, node) : AkanGraphDiff.#mark(node, "added");
|
|
67
|
+
if (next)
|
|
68
|
+
marked.set(node.id, next);
|
|
69
|
+
}
|
|
70
|
+
for (const node of base.nodes)
|
|
71
|
+
if (!headNodes.has(node.id))
|
|
72
|
+
marked.set(node.id, AkanGraphDiff.#mark(node, "removed"));
|
|
73
|
+
const baseKeys = new Set(base.edges.map(edgeKeyOf));
|
|
74
|
+
const headKeys = new Set(head.edges.map(edgeKeyOf));
|
|
75
|
+
const edges = [
|
|
76
|
+
...head.edges.flatMap((edge) => !baseKeys.has(edgeKeyOf(edge)) ? [{ ...edge, change: "added" }] : marked.has(edge.from) || marked.has(edge.to) ? [edge] : []),
|
|
77
|
+
...base.edges.filter((edge) => !headKeys.has(edgeKeyOf(edge))).map((edge) => ({ ...edge, change: "removed" }))
|
|
78
|
+
];
|
|
79
|
+
const context = edges.flatMap((edge) => [edge.from, edge.to]).filter((id) => !marked.has(id)).flatMap((id) => {
|
|
80
|
+
const node = headNodes.get(id) ?? baseNodes.get(id);
|
|
81
|
+
return node ? [node] : [];
|
|
82
|
+
});
|
|
83
|
+
const nodes = [...new Map([...marked.values(), ...context].map((node) => [node.id, node])).values()].map(AkanGraphDiff.#withoutDigest).sort(byId);
|
|
84
|
+
const summary = { added: 0, removed: 0, changed: 0 };
|
|
85
|
+
for (const node of marked.values())
|
|
86
|
+
if (node.change)
|
|
87
|
+
summary[node.change] += 1;
|
|
88
|
+
return { version: 1, nodes, edges, summary };
|
|
89
|
+
}
|
|
90
|
+
static #mark(node, change) {
|
|
91
|
+
return node.fields ? { ...node, change, fields: node.fields.map((field) => ({ ...field, change })) } : { ...node, change };
|
|
92
|
+
}
|
|
93
|
+
static #compare(before, after) {
|
|
94
|
+
const fields = AkanGraphDiff.#fieldsBetween(before.fields, after.fields);
|
|
95
|
+
const isChanged = isEdited(before, after) || before.label !== after.label || before.detail.join(`
|
|
96
|
+
`) !== after.detail.join(`
|
|
97
|
+
`) || (before.values ?? []).join(`
|
|
98
|
+
`) !== (after.values ?? []).join(`
|
|
99
|
+
`) || !!fields?.some((field) => field.change);
|
|
100
|
+
if (!isChanged)
|
|
101
|
+
return null;
|
|
102
|
+
return fields ? { ...after, change: "changed", fields } : { ...after, change: "changed" };
|
|
103
|
+
}
|
|
104
|
+
static #fieldsBetween(before, after) {
|
|
105
|
+
if (!before && !after)
|
|
106
|
+
return;
|
|
107
|
+
const previous = new Map((before ?? []).map((field) => [field.name, field]));
|
|
108
|
+
const names = new Set((after ?? []).map((field) => field.name));
|
|
109
|
+
return [
|
|
110
|
+
...(after ?? []).map((field) => {
|
|
111
|
+
const old = previous.get(field.name);
|
|
112
|
+
if (!old)
|
|
113
|
+
return { ...field, change: "added" };
|
|
114
|
+
return shapeOf(old) === shapeOf(field) && !isEdited(old, field) ? field : { ...field, change: "changed" };
|
|
115
|
+
}),
|
|
116
|
+
...(before ?? []).filter((field) => !names.has(field.name)).map((field) => ({ ...field, change: "removed" }))
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
static #withoutDigest({ digest: _digest, ...node }) {
|
|
120
|
+
return node.fields ? { ...node, fields: node.fields.map(({ digest: _field, ...field }) => field) } : node;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
49
123
|
// pkgs/@akanjs/language/akanGraph.ts
|
|
50
124
|
var capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
51
125
|
var lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
|
|
@@ -112,7 +186,7 @@ class GraphBuilder {
|
|
|
112
186
|
const refName = lowerFirst(filter.name.slice(0, -"Filter".length));
|
|
113
187
|
this.#addNode(this.#key("query", owner, refName), "query", owner, refName, "", filter.span);
|
|
114
188
|
for (const query of filter.queries)
|
|
115
|
-
this.#addNode(this.#key("query", owner, refName, query.name), "query", owner, refName, query.name, query.span);
|
|
189
|
+
this.#digest(this.#addNode(this.#key("query", owner, refName, query.name), "query", owner, refName, query.name, query.span), this.#source.readText(file.fileName), query.extent);
|
|
116
190
|
}
|
|
117
191
|
for (const slice of file.slices) {
|
|
118
192
|
const refName = slice.model;
|
|
@@ -189,6 +263,7 @@ class GraphBuilder {
|
|
|
189
263
|
#addMember(kind, owner, refName, member, label, fileName, detail = []) {
|
|
190
264
|
const id = this.#key(kind, owner, refName, member.name);
|
|
191
265
|
const node = this.#addNode(id, kind, owner, refName, member.name, member.span, detail, label);
|
|
266
|
+
this.#digest(node, this.#source.readText(fileName), member.extent);
|
|
192
267
|
this.#contexts.set(fileName, [
|
|
193
268
|
...this.#contexts.get(fileName) ?? [],
|
|
194
269
|
{ id, kind, owner, module: refName, extent: member.extent }
|
|
@@ -197,6 +272,22 @@ class GraphBuilder {
|
|
|
197
272
|
this.#members.set(byName, [...this.#members.get(byName) ?? [], node]);
|
|
198
273
|
return node;
|
|
199
274
|
}
|
|
275
|
+
#hashOf(text, { start, end }) {
|
|
276
|
+
if (!this.#source.digests || text === undefined)
|
|
277
|
+
return null;
|
|
278
|
+
let hash = 2166136261;
|
|
279
|
+
for (let idx = start;idx < end; idx++) {
|
|
280
|
+
const code = text.charCodeAt(idx);
|
|
281
|
+
if (code !== 13)
|
|
282
|
+
hash = Math.imul(hash ^ code, 16777619);
|
|
283
|
+
}
|
|
284
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
285
|
+
}
|
|
286
|
+
#digest(node, text, extent) {
|
|
287
|
+
const digest = node.digest ? null : this.#hashOf(text, extent);
|
|
288
|
+
if (digest)
|
|
289
|
+
node.digest = digest;
|
|
290
|
+
}
|
|
200
291
|
#addEdge(from, to, kind, label, at) {
|
|
201
292
|
if (from === to)
|
|
202
293
|
return;
|
|
@@ -220,7 +311,8 @@ ${label ?? ""}`;
|
|
|
220
311
|
this.#addEdge(node.id, id, "guard", role, node.at);
|
|
221
312
|
}
|
|
222
313
|
#moduleDirOf(fileName) {
|
|
223
|
-
|
|
314
|
+
const dir = this.#source.relative(fileName).match(moduleDirPattern)?.[1] ?? null;
|
|
315
|
+
return dir?.startsWith("_") && !dir.startsWith("__") ? dir.slice(1) : dir;
|
|
224
316
|
}
|
|
225
317
|
#fieldsOf(constant, scope) {
|
|
226
318
|
const fields = new Map;
|
|
@@ -234,17 +326,21 @@ ${label ?? ""}`;
|
|
|
234
326
|
if (parentClass)
|
|
235
327
|
visit(parentClass);
|
|
236
328
|
}
|
|
237
|
-
for (const field of current.fields)
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
329
|
+
for (const field of current.fields) {
|
|
330
|
+
if (fields.has(field.name))
|
|
331
|
+
continue;
|
|
332
|
+
const digest = this.#hashOf(this.#source.readText(field.span.fileName), field.extent);
|
|
333
|
+
fields.set(field.name, {
|
|
334
|
+
name: field.name,
|
|
335
|
+
type: field.type,
|
|
336
|
+
array: field.array,
|
|
337
|
+
optional: field.optional,
|
|
338
|
+
ref: null,
|
|
339
|
+
declaredIn: current.name,
|
|
340
|
+
at: this.#location(field.span),
|
|
341
|
+
...digest ? { digest } : {}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
248
344
|
};
|
|
249
345
|
visit(constant);
|
|
250
346
|
return [...fields.values()];
|
|
@@ -307,8 +403,10 @@ st
|
|
|
307
403
|
const targets = resolved.length ? resolved : this.#plainTargetsOf(chain, identifier.text, context, owner);
|
|
308
404
|
if (!targets.length)
|
|
309
405
|
return;
|
|
310
|
-
if (context.pending && !this.#nodes.has(context.id))
|
|
406
|
+
if (context.pending && !this.#nodes.has(context.id)) {
|
|
311
407
|
this.#nodes.set(context.id, context.pending);
|
|
408
|
+
this.#digest(context.pending, source.text, context.extent);
|
|
409
|
+
}
|
|
312
410
|
const own = targets.filter((id) => this.#nodes.get(id)?.owner === owner);
|
|
313
411
|
const kind = context.kind === "component" ? "uses" : chain?.[0] === "fetch" ? "fetches" : "calls";
|
|
314
412
|
const position = source.getLineAndCharacterOfPosition(identifier.getStart(source));
|
|
@@ -503,7 +601,7 @@ var createFactReader = (ts, ast) => {
|
|
|
503
601
|
const objectKeys = (source, object) => (object?.properties ?? []).flatMap((property) => {
|
|
504
602
|
const name = ast.propertyName(property);
|
|
505
603
|
const node = keyName(property);
|
|
506
|
-
return name && node ? [{ name, span: spanOf(source, node) }] : [];
|
|
604
|
+
return name && node ? [{ name, span: spanOf(source, node), extent: extentOf(source, property) }] : [];
|
|
507
605
|
});
|
|
508
606
|
const sliceKeysOf = (source, builder) => (returnedObject(builder)?.properties ?? []).flatMap((property) => {
|
|
509
607
|
if (!ts.isPropertyAssignment(property))
|
|
@@ -578,7 +676,13 @@ var createFactReader = (ts, ast) => {
|
|
|
578
676
|
return [];
|
|
579
677
|
const { root, methods } = ts.isPropertyAssignment(property) ? chainOf(property.initializer) : { root: null, methods: [] };
|
|
580
678
|
return [
|
|
581
|
-
{
|
|
679
|
+
{
|
|
680
|
+
name,
|
|
681
|
+
span: spanOf(source, node),
|
|
682
|
+
extent: extentOf(source, property),
|
|
683
|
+
...typeOf(root?.arguments[0]),
|
|
684
|
+
optional: methods.includes("optional")
|
|
685
|
+
}
|
|
582
686
|
];
|
|
583
687
|
});
|
|
584
688
|
const constantOf = (source, name, call) => {
|
|
@@ -1196,8 +1300,9 @@ ${[...new Set(text.match(wordPattern))].join(`
|
|
|
1196
1300
|
referencesAt(givenName, offset, readText) {
|
|
1197
1301
|
return this.#usesOf(this.#sourceEntriesAt(pathOf(givenName), offset), readText).map((use) => use.span);
|
|
1198
1302
|
}
|
|
1199
|
-
graph(readText) {
|
|
1303
|
+
graph(readText, { digests = false } = {}) {
|
|
1200
1304
|
return new GraphBuilder({
|
|
1305
|
+
digests,
|
|
1201
1306
|
ts: this.#ts,
|
|
1202
1307
|
facts: [...this.#facts].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, facts]) => facts),
|
|
1203
1308
|
files: [...this.#fileWords].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0),
|
package/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export type { AkanGraph, AkanGraphEdge, AkanGraphEdgeKind, AkanGraphField, AkanGraphLocation, AkanGraphNode, AkanGraphNodeKind, } from "akanjs/common/akanGraph.ts";
|
|
1
|
+
export type { AkanChangedFile, AkanChangeMap, AkanGraph, AkanGraphChange, AkanGraphEdge, AkanGraphEdgeKind, AkanGraphField, AkanGraphLocation, AkanGraphNode, AkanGraphNodeKind, } from "akanjs/common/akanGraph.ts";
|
|
2
|
+
export { type AkanGraphChanges, AkanGraphDiff } from "./akanGraphDiff";
|
|
2
3
|
export { type AkanHover, AkanIndex, type AkanIndexOptions, type AkanRenameEdit, type AkanSourceKey } from "./akanIndex";
|
|
3
4
|
export { type AkanAst, createAst, type Ts } from "./ast";
|
|
4
5
|
export { type AkanFactReader, type AkanSpan, type ConstantClassFact, createFactReader, type EndpointClassFact, type EndpointKeyFact, type EnumClassFact, type Extent, type FieldFact, type FileFacts, type FilterClassFact, type MemberFact, type ModuleClassFact, type NamedFact, ownerOf, type ServiceClassFact, type SliceClassFact, type SliceKeyFact, type StoreClassFact, } from "./moduleFacts";
|
package/index.js
CHANGED
|
@@ -1,3 +1,76 @@
|
|
|
1
|
+
// pkgs/@akanjs/language/akanGraphDiff.ts
|
|
2
|
+
var edgeKeyOf = ({ from, to, kind, label }) => `${from}
|
|
3
|
+
${to}
|
|
4
|
+
${kind}
|
|
5
|
+
${label ?? ""}`;
|
|
6
|
+
var shapeOf = ({ type, array, optional, declaredIn }) => `${type ?? ""}${array ? "[]" : ""}${optional ? "?" : ""} ${declaredIn}`;
|
|
7
|
+
var isEdited = (before, after) => before.digest !== undefined && after.digest !== undefined && before.digest !== after.digest;
|
|
8
|
+
var byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
9
|
+
|
|
10
|
+
class AkanGraphDiff {
|
|
11
|
+
static between(base, head) {
|
|
12
|
+
const baseNodes = new Map(base.nodes.map((node) => [node.id, node]));
|
|
13
|
+
const headNodes = new Map(head.nodes.map((node) => [node.id, node]));
|
|
14
|
+
const marked = new Map;
|
|
15
|
+
for (const node of head.nodes) {
|
|
16
|
+
const before = baseNodes.get(node.id);
|
|
17
|
+
const next = before ? AkanGraphDiff.#compare(before, node) : AkanGraphDiff.#mark(node, "added");
|
|
18
|
+
if (next)
|
|
19
|
+
marked.set(node.id, next);
|
|
20
|
+
}
|
|
21
|
+
for (const node of base.nodes)
|
|
22
|
+
if (!headNodes.has(node.id))
|
|
23
|
+
marked.set(node.id, AkanGraphDiff.#mark(node, "removed"));
|
|
24
|
+
const baseKeys = new Set(base.edges.map(edgeKeyOf));
|
|
25
|
+
const headKeys = new Set(head.edges.map(edgeKeyOf));
|
|
26
|
+
const edges = [
|
|
27
|
+
...head.edges.flatMap((edge) => !baseKeys.has(edgeKeyOf(edge)) ? [{ ...edge, change: "added" }] : marked.has(edge.from) || marked.has(edge.to) ? [edge] : []),
|
|
28
|
+
...base.edges.filter((edge) => !headKeys.has(edgeKeyOf(edge))).map((edge) => ({ ...edge, change: "removed" }))
|
|
29
|
+
];
|
|
30
|
+
const context = edges.flatMap((edge) => [edge.from, edge.to]).filter((id) => !marked.has(id)).flatMap((id) => {
|
|
31
|
+
const node = headNodes.get(id) ?? baseNodes.get(id);
|
|
32
|
+
return node ? [node] : [];
|
|
33
|
+
});
|
|
34
|
+
const nodes = [...new Map([...marked.values(), ...context].map((node) => [node.id, node])).values()].map(AkanGraphDiff.#withoutDigest).sort(byId);
|
|
35
|
+
const summary = { added: 0, removed: 0, changed: 0 };
|
|
36
|
+
for (const node of marked.values())
|
|
37
|
+
if (node.change)
|
|
38
|
+
summary[node.change] += 1;
|
|
39
|
+
return { version: 1, nodes, edges, summary };
|
|
40
|
+
}
|
|
41
|
+
static #mark(node, change) {
|
|
42
|
+
return node.fields ? { ...node, change, fields: node.fields.map((field) => ({ ...field, change })) } : { ...node, change };
|
|
43
|
+
}
|
|
44
|
+
static #compare(before, after) {
|
|
45
|
+
const fields = AkanGraphDiff.#fieldsBetween(before.fields, after.fields);
|
|
46
|
+
const isChanged = isEdited(before, after) || before.label !== after.label || before.detail.join(`
|
|
47
|
+
`) !== after.detail.join(`
|
|
48
|
+
`) || (before.values ?? []).join(`
|
|
49
|
+
`) !== (after.values ?? []).join(`
|
|
50
|
+
`) || !!fields?.some((field) => field.change);
|
|
51
|
+
if (!isChanged)
|
|
52
|
+
return null;
|
|
53
|
+
return fields ? { ...after, change: "changed", fields } : { ...after, change: "changed" };
|
|
54
|
+
}
|
|
55
|
+
static #fieldsBetween(before, after) {
|
|
56
|
+
if (!before && !after)
|
|
57
|
+
return;
|
|
58
|
+
const previous = new Map((before ?? []).map((field) => [field.name, field]));
|
|
59
|
+
const names = new Set((after ?? []).map((field) => field.name));
|
|
60
|
+
return [
|
|
61
|
+
...(after ?? []).map((field) => {
|
|
62
|
+
const old = previous.get(field.name);
|
|
63
|
+
if (!old)
|
|
64
|
+
return { ...field, change: "added" };
|
|
65
|
+
return shapeOf(old) === shapeOf(field) && !isEdited(old, field) ? field : { ...field, change: "changed" };
|
|
66
|
+
}),
|
|
67
|
+
...(before ?? []).filter((field) => !names.has(field.name)).map((field) => ({ ...field, change: "removed" }))
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
static #withoutDigest({ digest: _digest, ...node }) {
|
|
71
|
+
return node.fields ? { ...node, fields: node.fields.map(({ digest: _field, ...field }) => field) } : node;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
1
74
|
// pkgs/@akanjs/language/akanGraph.ts
|
|
2
75
|
var capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
3
76
|
var lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
|
|
@@ -64,7 +137,7 @@ class GraphBuilder {
|
|
|
64
137
|
const refName = lowerFirst(filter.name.slice(0, -"Filter".length));
|
|
65
138
|
this.#addNode(this.#key("query", owner, refName), "query", owner, refName, "", filter.span);
|
|
66
139
|
for (const query of filter.queries)
|
|
67
|
-
this.#addNode(this.#key("query", owner, refName, query.name), "query", owner, refName, query.name, query.span);
|
|
140
|
+
this.#digest(this.#addNode(this.#key("query", owner, refName, query.name), "query", owner, refName, query.name, query.span), this.#source.readText(file.fileName), query.extent);
|
|
68
141
|
}
|
|
69
142
|
for (const slice of file.slices) {
|
|
70
143
|
const refName = slice.model;
|
|
@@ -141,6 +214,7 @@ class GraphBuilder {
|
|
|
141
214
|
#addMember(kind, owner, refName, member, label, fileName, detail = []) {
|
|
142
215
|
const id = this.#key(kind, owner, refName, member.name);
|
|
143
216
|
const node = this.#addNode(id, kind, owner, refName, member.name, member.span, detail, label);
|
|
217
|
+
this.#digest(node, this.#source.readText(fileName), member.extent);
|
|
144
218
|
this.#contexts.set(fileName, [
|
|
145
219
|
...this.#contexts.get(fileName) ?? [],
|
|
146
220
|
{ id, kind, owner, module: refName, extent: member.extent }
|
|
@@ -149,6 +223,22 @@ class GraphBuilder {
|
|
|
149
223
|
this.#members.set(byName, [...this.#members.get(byName) ?? [], node]);
|
|
150
224
|
return node;
|
|
151
225
|
}
|
|
226
|
+
#hashOf(text, { start, end }) {
|
|
227
|
+
if (!this.#source.digests || text === undefined)
|
|
228
|
+
return null;
|
|
229
|
+
let hash = 2166136261;
|
|
230
|
+
for (let idx = start;idx < end; idx++) {
|
|
231
|
+
const code = text.charCodeAt(idx);
|
|
232
|
+
if (code !== 13)
|
|
233
|
+
hash = Math.imul(hash ^ code, 16777619);
|
|
234
|
+
}
|
|
235
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
236
|
+
}
|
|
237
|
+
#digest(node, text, extent) {
|
|
238
|
+
const digest = node.digest ? null : this.#hashOf(text, extent);
|
|
239
|
+
if (digest)
|
|
240
|
+
node.digest = digest;
|
|
241
|
+
}
|
|
152
242
|
#addEdge(from, to, kind, label, at) {
|
|
153
243
|
if (from === to)
|
|
154
244
|
return;
|
|
@@ -172,7 +262,8 @@ ${label ?? ""}`;
|
|
|
172
262
|
this.#addEdge(node.id, id, "guard", role, node.at);
|
|
173
263
|
}
|
|
174
264
|
#moduleDirOf(fileName) {
|
|
175
|
-
|
|
265
|
+
const dir = this.#source.relative(fileName).match(moduleDirPattern)?.[1] ?? null;
|
|
266
|
+
return dir?.startsWith("_") && !dir.startsWith("__") ? dir.slice(1) : dir;
|
|
176
267
|
}
|
|
177
268
|
#fieldsOf(constant, scope) {
|
|
178
269
|
const fields = new Map;
|
|
@@ -186,17 +277,21 @@ ${label ?? ""}`;
|
|
|
186
277
|
if (parentClass)
|
|
187
278
|
visit(parentClass);
|
|
188
279
|
}
|
|
189
|
-
for (const field of current.fields)
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
280
|
+
for (const field of current.fields) {
|
|
281
|
+
if (fields.has(field.name))
|
|
282
|
+
continue;
|
|
283
|
+
const digest = this.#hashOf(this.#source.readText(field.span.fileName), field.extent);
|
|
284
|
+
fields.set(field.name, {
|
|
285
|
+
name: field.name,
|
|
286
|
+
type: field.type,
|
|
287
|
+
array: field.array,
|
|
288
|
+
optional: field.optional,
|
|
289
|
+
ref: null,
|
|
290
|
+
declaredIn: current.name,
|
|
291
|
+
at: this.#location(field.span),
|
|
292
|
+
...digest ? { digest } : {}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
200
295
|
};
|
|
201
296
|
visit(constant);
|
|
202
297
|
return [...fields.values()];
|
|
@@ -259,8 +354,10 @@ st
|
|
|
259
354
|
const targets = resolved.length ? resolved : this.#plainTargetsOf(chain, identifier.text, context, owner);
|
|
260
355
|
if (!targets.length)
|
|
261
356
|
return;
|
|
262
|
-
if (context.pending && !this.#nodes.has(context.id))
|
|
357
|
+
if (context.pending && !this.#nodes.has(context.id)) {
|
|
263
358
|
this.#nodes.set(context.id, context.pending);
|
|
359
|
+
this.#digest(context.pending, source.text, context.extent);
|
|
360
|
+
}
|
|
264
361
|
const own = targets.filter((id) => this.#nodes.get(id)?.owner === owner);
|
|
265
362
|
const kind = context.kind === "component" ? "uses" : chain?.[0] === "fetch" ? "fetches" : "calls";
|
|
266
363
|
const position = source.getLineAndCharacterOfPosition(identifier.getStart(source));
|
|
@@ -455,7 +552,7 @@ var createFactReader = (ts, ast) => {
|
|
|
455
552
|
const objectKeys = (source, object) => (object?.properties ?? []).flatMap((property) => {
|
|
456
553
|
const name = ast.propertyName(property);
|
|
457
554
|
const node = keyName(property);
|
|
458
|
-
return name && node ? [{ name, span: spanOf(source, node) }] : [];
|
|
555
|
+
return name && node ? [{ name, span: spanOf(source, node), extent: extentOf(source, property) }] : [];
|
|
459
556
|
});
|
|
460
557
|
const sliceKeysOf = (source, builder) => (returnedObject(builder)?.properties ?? []).flatMap((property) => {
|
|
461
558
|
if (!ts.isPropertyAssignment(property))
|
|
@@ -530,7 +627,13 @@ var createFactReader = (ts, ast) => {
|
|
|
530
627
|
return [];
|
|
531
628
|
const { root, methods } = ts.isPropertyAssignment(property) ? chainOf(property.initializer) : { root: null, methods: [] };
|
|
532
629
|
return [
|
|
533
|
-
{
|
|
630
|
+
{
|
|
631
|
+
name,
|
|
632
|
+
span: spanOf(source, node),
|
|
633
|
+
extent: extentOf(source, property),
|
|
634
|
+
...typeOf(root?.arguments[0]),
|
|
635
|
+
optional: methods.includes("optional")
|
|
636
|
+
}
|
|
534
637
|
];
|
|
535
638
|
});
|
|
536
639
|
const constantOf = (source, name, call) => {
|
|
@@ -1148,8 +1251,9 @@ ${[...new Set(text.match(wordPattern))].join(`
|
|
|
1148
1251
|
referencesAt(givenName, offset, readText) {
|
|
1149
1252
|
return this.#usesOf(this.#sourceEntriesAt(pathOf(givenName), offset), readText).map((use) => use.span);
|
|
1150
1253
|
}
|
|
1151
|
-
graph(readText) {
|
|
1254
|
+
graph(readText, { digests = false } = {}) {
|
|
1152
1255
|
return new GraphBuilder({
|
|
1256
|
+
digests,
|
|
1153
1257
|
ts: this.#ts,
|
|
1154
1258
|
facts: [...this.#facts].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, facts]) => facts),
|
|
1155
1259
|
files: [...this.#fileWords].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0),
|
|
@@ -1410,6 +1514,7 @@ ${dictionary.className}
|
|
|
1410
1514
|
}
|
|
1411
1515
|
}
|
|
1412
1516
|
export {
|
|
1517
|
+
AkanGraphDiff,
|
|
1413
1518
|
AkanIndex,
|
|
1414
1519
|
createAst,
|
|
1415
1520
|
createFactReader,
|
package/moduleFacts.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export interface EndpointKeyFact extends MemberFact {
|
|
|
31
31
|
argCount: number;
|
|
32
32
|
}
|
|
33
33
|
export interface FieldFact extends NamedFact {
|
|
34
|
+
extent: Extent;
|
|
34
35
|
type: string | null;
|
|
35
36
|
array: boolean;
|
|
36
37
|
optional: boolean;
|
|
@@ -43,7 +44,7 @@ export interface EnumClassFact extends NamedFact {
|
|
|
43
44
|
values: string[];
|
|
44
45
|
}
|
|
45
46
|
export interface FilterClassFact extends NamedFact {
|
|
46
|
-
queries:
|
|
47
|
+
queries: MemberFact[];
|
|
47
48
|
}
|
|
48
49
|
export interface ModuleClassFact extends NamedFact {
|
|
49
50
|
model: string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/language",
|
|
3
|
-
"version": "3.1.2-rc.
|
|
3
|
+
"version": "3.1.2-rc.4",
|
|
4
4
|
"description": "Editor-independent Akan.js language core: resolves derived names (slices, queries, CRUD, form setters) back to the source key that produced them.",
|
|
5
5
|
"sourceType": "module",
|
|
6
6
|
"type": "module",
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
"module": "./index.js",
|
|
27
27
|
"types": "./index.d.ts",
|
|
28
28
|
"sideEffects": false,
|
|
29
|
+
"bin": {
|
|
30
|
+
"akan-language-server": "./server.js"
|
|
31
|
+
},
|
|
29
32
|
"exports": {
|
|
30
33
|
".": {
|
|
31
34
|
"types": "./index.d.ts",
|