@aprimediet/codewalker 1.1.0 → 1.2.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.
@@ -0,0 +1,213 @@
1
+ /**
2
+ * .d.ts symbol extraction — regex/line-based, PURE (no I/O).
3
+ *
4
+ * Extracts top-level exported declarations from a `.d.ts` source string.
5
+ * Modeled on src/extract/regex.ts. Returns LibSymbol[] for a given library.
6
+ *
7
+ * Handles these export forms:
8
+ * export declare function|const|class|enum|namespace
9
+ * export function|const|class|interface|type|enum|namespace
10
+ * export abstract class
11
+ * export { a, b as c } from "..." (reexport)
12
+ * export { a, b } (local reexport)
13
+ * export * from "..." (star reexport → name: "*")
14
+ * export default … (name: "default")
15
+ *
16
+ * Non-exported declarations are ignored. Leading JSDoc is captured.
17
+ */
18
+
19
+ import type { LibSymbol, SymbolKind } from "../types.ts";
20
+ import { extractDocComment } from "../extract/docs.ts";
21
+
22
+ /**
23
+ * Extract top-level exported symbols from a .d.ts source string.
24
+ *
25
+ * @param source - Full .d.ts file content.
26
+ * @param lib - Library name.
27
+ * @param version - Installed version.
28
+ * @returns Array of extracted LibSymbol objects.
29
+ */
30
+ export function extractDtsSymbols(
31
+ source: string,
32
+ lib: string,
33
+ version: string,
34
+ ): LibSymbol[] {
35
+ const lines = source.split("\n");
36
+ const symbols: LibSymbol[] = [];
37
+ let inBlockComment = false;
38
+
39
+ for (let i = 0; i < lines.length; i++) {
40
+ const rawLine = lines[i] as string;
41
+ const trimmed = rawLine.trim();
42
+
43
+ // Track block comments
44
+ if (inBlockComment) {
45
+ if (trimmed.includes("*/")) inBlockComment = false;
46
+ continue;
47
+ }
48
+ if (trimmed.startsWith("/*") || trimmed.startsWith("/**")) {
49
+ if (!trimmed.includes("*/")) inBlockComment = true;
50
+ continue;
51
+ }
52
+ if (trimmed.startsWith("//")) continue;
53
+ if (!trimmed) continue;
54
+
55
+ // ── Named exports: export [declare] [abstract] <kind> <name> ──
56
+ const namedExport = tryExtractNamedExport(trimmed, lines, i, lib, version, source);
57
+ if (namedExport) {
58
+ symbols.push(namedExport);
59
+ continue;
60
+ }
61
+
62
+ // ── export default … (name always "default") ──
63
+ const defaultExport = tryExtractDefaultExport(trimmed, lines, i, lib, version, source);
64
+ if (defaultExport) {
65
+ symbols.push(defaultExport);
66
+ continue;
67
+ }
68
+
69
+ // ── Re-exports: export { … } or export * from … ──
70
+ const reExport = tryExtractReexport(trimmed, lines, i, lib, version, source);
71
+ if (reExport) {
72
+ symbols.push(...reExport);
73
+ continue;
74
+ }
75
+ }
76
+
77
+ return symbols;
78
+ }
79
+
80
+ /**
81
+ * Try to extract a named export declaration (function, const, class, interface, type, enum, namespace).
82
+ */
83
+ function tryExtractNamedExport(
84
+ trimmed: string,
85
+ lines: string[],
86
+ i: number,
87
+ lib: string,
88
+ version: string,
89
+ source: string,
90
+ ): LibSymbol | null {
91
+ // Order matters: more specific before less specific.
92
+ // Each pattern: regex with named-kind capture, and a name capture group index.
93
+ const patterns: Array<{ regex: RegExp; kind: SymbolKind; nameIdx: number }> = [
94
+ // function: export [declare] [abstract] [async] function [<T>] name
95
+ { regex: /^export\s+(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?function\s+(?:<[^>]+>\s+)?(\w+)/, kind: "function", nameIdx: 1 },
96
+ // class: export [declare] [abstract] class name
97
+ { regex: /^export\s+(?:declare\s+)?(?:abstract\s+)?class\s+(\w+)/, kind: "class", nameIdx: 1 },
98
+ // interface: export interface name
99
+ { regex: /^export\s+interface\s+(\w+)/, kind: "interface", nameIdx: 1 },
100
+ // type: export type name [<...>] =
101
+ { regex: /^export\s+type\s+(\w+)/, kind: "type", nameIdx: 1 },
102
+ // enum: export [declare] enum name
103
+ { regex: /^export\s+(?:declare\s+)?enum\s+(\w+)/, kind: "enum", nameIdx: 1 },
104
+ // namespace: export [declare] namespace name
105
+ { regex: /^export\s+(?:declare\s+)?namespace\s+(\w+)/, kind: "namespace", nameIdx: 1 },
106
+ // const/let/var: export [declare] const name
107
+ { regex: /^export\s+(?:declare\s+)?(?:const|let|var)\s+(\w+)/, kind: "const", nameIdx: 1 },
108
+ ];
109
+
110
+ for (const p of patterns) {
111
+ const m = trimmed.match(p.regex);
112
+ if (m?.[p.nameIdx]) {
113
+ return makeSymbol(lines, i, p.kind, m[p.nameIdx]!, lib, version, source);
114
+ }
115
+ }
116
+
117
+ return null;
118
+ }
119
+
120
+ /**
121
+ * Try to extract `export default …`.
122
+ * Name is always "default". Kind is determined by what follows:
123
+ * function → "function", class → "class", object/expr → "const".
124
+ */
125
+ function tryExtractDefaultExport(
126
+ trimmed: string,
127
+ lines: string[],
128
+ i: number,
129
+ lib: string,
130
+ version: string,
131
+ source: string,
132
+ ): LibSymbol | null {
133
+ if (!trimmed.startsWith("export default")) return null;
134
+
135
+ let kind: SymbolKind = "const";
136
+
137
+ if (/^export\s+default\s+(?:async\s+)?function\s/.test(trimmed)) {
138
+ kind = "function";
139
+ } else if (/^export\s+default\s+(?:abstract\s+)?class\s/.test(trimmed)) {
140
+ kind = "class";
141
+ } else if (/^export\s+default\s+interface\s/.test(trimmed)) {
142
+ kind = "interface";
143
+ }
144
+
145
+ return makeSymbol(lines, i, kind, "default", lib, version, source);
146
+ }
147
+
148
+ /**
149
+ * Try to extract re-export declarations:
150
+ * export { a, b as c } [from "..."]
151
+ * export * from "..."
152
+ */
153
+ function tryExtractReexport(
154
+ trimmed: string,
155
+ lines: string[],
156
+ i: number,
157
+ lib: string,
158
+ version: string,
159
+ source: string,
160
+ ): LibSymbol[] | null {
161
+ // export { a, b as c } [from "..."]
162
+ const braceReexport = trimmed.match(/^export\s+\{([^}]+)\}(?:\s+from\s+["'][^"']*["'])?\s*;?\s*$/);
163
+ if (braceReexport) {
164
+ const names = braceReexport[1]!.split(",").map(s => s.trim()).filter(Boolean);
165
+ return names.map((entry) => {
166
+ const aliasMatch = entry.match(/^(\w+)\s+as\s+(\w+)$/);
167
+ const name = aliasMatch?.[2] ?? entry;
168
+ return makeSymbol(lines, i, "reexport", name, lib, version, source);
169
+ });
170
+ }
171
+
172
+ // export * from "..."
173
+ if (/^export\s+\*\s+from\s+["'][^"']*["']\s*;?\s*$/.test(trimmed)) {
174
+ return [makeSymbol(lines, i, "reexport", "*", lib, version, source)];
175
+ }
176
+
177
+ return null;
178
+ }
179
+
180
+ /**
181
+ * Build a LibSymbol from a declaration line.
182
+ * Signature is the declaration line with the body (from `{` onward) stripped.
183
+ */
184
+ function makeSymbol(
185
+ lines: string[],
186
+ lineIndex: number,
187
+ kind: SymbolKind,
188
+ name: string,
189
+ lib: string,
190
+ version: string,
191
+ source: string,
192
+ ): LibSymbol {
193
+ const rawLine = (lines[lineIndex] as string).trim();
194
+
195
+ // Signature: first line, strip everything from the first `{` onward
196
+ const sigStart = rawLine.indexOf("{");
197
+ const signature = (sigStart >= 0 ? rawLine.slice(0, sigStart) : rawLine).trim();
198
+
199
+ // Capture JSDoc
200
+ const doc = extractDocComment(source, lineIndex + 1);
201
+ const summary = (doc.split("\n")[0] || "").trim();
202
+
203
+ return {
204
+ lib,
205
+ version,
206
+ name,
207
+ kind,
208
+ signature,
209
+ doc,
210
+ summary,
211
+ card_path: "",
212
+ };
213
+ }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Tests for libs/indexer.ts — library indexer.
3
+ *
4
+ * Integration tests using a fixture project with a fake node_modules.
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import * as os from "node:os";
11
+ import { indexLibraries, rebuildLibDbFromCards } from "./indexer.ts";
12
+ import { openDb, searchLibSymbols, getMeta } from "../db.ts";
13
+
14
+ describe("indexLibraries", () => {
15
+ let tmpDir: string;
16
+ let projectRoot: string;
17
+ let libsDir: string;
18
+ let dbPath: string;
19
+ let nodeModulesDir: string;
20
+
21
+ beforeEach(() => {
22
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "cw-idx-"));
23
+ projectRoot = path.join(tmpDir, "project");
24
+ libsDir = path.join(tmpDir, "codewalker", "entries", "libs");
25
+ dbPath = path.join(tmpDir, "codewalker", "index.db");
26
+ nodeModulesDir = path.join(projectRoot, "node_modules");
27
+
28
+ fs.mkdirSync(nodeModulesDir, { recursive: true });
29
+ fs.mkdirSync(libsDir, { recursive: true });
30
+
31
+ // Write a project package.json
32
+ fs.writeFileSync(
33
+ path.join(projectRoot, "package.json"),
34
+ JSON.stringify({
35
+ name: "test-project",
36
+ version: "1.0.0",
37
+ dependencies: {
38
+ "typed-pkg": "^1.0.0",
39
+ "no-dts-pkg": "^2.0.0",
40
+ },
41
+ }),
42
+ );
43
+ });
44
+
45
+ afterEach(() => {
46
+ fs.rmSync(tmpDir, { recursive: true, force: true });
47
+ });
48
+
49
+ function installTypedPkg(): void {
50
+ const pkgDir = path.join(nodeModulesDir, "typed-pkg");
51
+ fs.mkdirSync(path.join(pkgDir, "dist"), { recursive: true });
52
+ fs.writeFileSync(
53
+ path.join(pkgDir, "package.json"),
54
+ JSON.stringify({ name: "typed-pkg", version: "1.2.0", types: "dist/index.d.ts" }),
55
+ );
56
+ fs.writeFileSync(
57
+ path.join(pkgDir, "dist", "index.d.ts"),
58
+ [
59
+ "/** A typed greeting function. */",
60
+ "export declare function greet(name: string): string;",
61
+ "/** Configuration options. */",
62
+ "export interface Config { port: number; }",
63
+ "export const VERSION: string;",
64
+ ].join("\n"),
65
+ );
66
+ fs.writeFileSync(pkgDir + "/README.md", "# typed-pkg\nA typed package for testing.\n");
67
+ }
68
+
69
+ function installNoDtsPkg(): void {
70
+ const pkgDir = path.join(nodeModulesDir, "no-dts-pkg");
71
+ fs.mkdirSync(pkgDir);
72
+ fs.writeFileSync(
73
+ path.join(pkgDir, "package.json"),
74
+ JSON.stringify({ name: "no-dts-pkg", version: "2.1.0", main: "index.js" }),
75
+ );
76
+ fs.writeFileSync(path.join(pkgDir, "index.js"), "module.exports = {};\n");
77
+ fs.writeFileSync(path.join(pkgDir, "README.md"), "# no-dts-pkg\nA JS-only package.\n");
78
+ }
79
+
80
+ it("indexes typed dependency: writes cards and populates DB", async () => {
81
+ installTypedPkg();
82
+
83
+ const result = await indexLibraries({ projectRoot, libsDir, dbPath });
84
+ expect(result.indexed).toBe(1);
85
+ expect(result.symbols).toBe(3);
86
+ expect(result.errors).toBe(0);
87
+
88
+ // Check cards exist
89
+ const pkgCardDir = path.join(libsDir, "typed-pkg@1.2.0");
90
+ expect(fs.existsSync(pkgCardDir)).toBe(true);
91
+ const cardFiles = fs.readdirSync(pkgCardDir);
92
+ // greet, Config, VERSION
93
+ expect(cardFiles).toHaveLength(3);
94
+
95
+ // Check DB
96
+ const db = openDb(dbPath);
97
+ const symbols = searchLibSymbols(db, "", undefined, 10);
98
+ expect(symbols).toHaveLength(3);
99
+ expect(symbols.map(s => s.name)).toContain("greet");
100
+ expect(symbols.map(s => s.name)).toContain("Config");
101
+ expect(symbols.map(s => s.name)).toContain("VERSION");
102
+ expect(symbols[0]!.lib).toBe("typed-pkg");
103
+ expect(symbols[0]!.version).toBe("1.2.0");
104
+ db.close();
105
+ });
106
+
107
+ it("indexes a README-only dependency (no .d.ts)", async () => {
108
+ installNoDtsPkg();
109
+
110
+ const result = await indexLibraries({ projectRoot, libsDir, dbPath });
111
+ // Only no-dts-pkg is installed; typed-pkg missing from node_modules so skipped
112
+ expect(result.indexed).toBe(1);
113
+ expect(result.symbols).toBeGreaterThanOrEqual(1); // at least the module card
114
+ expect(result.errors).toBe(0);
115
+
116
+ // Check README-only package got a module card
117
+ const pkgCardDir = path.join(libsDir, "no-dts-pkg@2.1.0");
118
+ expect(fs.existsSync(pkgCardDir)).toBe(true);
119
+ const cardFiles = fs.readdirSync(pkgCardDir);
120
+ // Should have a module card (README summary)
121
+ expect(cardFiles.length).toBeGreaterThanOrEqual(1);
122
+ });
123
+
124
+ it("is idempotent: re-running produces no duplicates", async () => {
125
+ installTypedPkg();
126
+
127
+ await indexLibraries({ projectRoot, libsDir, dbPath });
128
+ await indexLibraries({ projectRoot, libsDir, dbPath });
129
+
130
+ const db = openDb(dbPath);
131
+ const symbols = searchLibSymbols(db, "", undefined, 10);
132
+ expect(symbols).toHaveLength(3);
133
+ db.close();
134
+ });
135
+
136
+ it("handles missing node_modules gracefully", async () => {
137
+ // Remove node_modules
138
+ fs.rmSync(nodeModulesDir, { recursive: true, force: true });
139
+
140
+ const result = await indexLibraries({ projectRoot, libsDir, dbPath });
141
+ expect(result.indexed).toBe(0);
142
+ expect(result.symbols).toBe(0);
143
+ expect(result.errors).toBe(0);
144
+ });
145
+
146
+ it("handles a dep that is missing from node_modules with a logged note", async () => {
147
+ // Only install one of the two deps
148
+ installTypedPkg();
149
+ // no-dts-pkg is missing from node_modules
150
+
151
+ const result = await indexLibraries({ projectRoot, libsDir, dbPath });
152
+ // Should index typed-pkg, skip no-dts-pkg
153
+ expect(result.indexed).toBe(1);
154
+ expect(result.errors).toBe(0);
155
+ });
156
+
157
+ it("includes devDependencies when includeDev=true", async () => {
158
+ // Add a dev dependency
159
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf-8"));
160
+ pkgJson.devDependencies = { "dev-pkg": "^3.0.0" };
161
+ fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(pkgJson));
162
+
163
+ // Install dev-pkg (typed)
164
+ const devPkgDir = path.join(nodeModulesDir, "dev-pkg");
165
+ fs.mkdirSync(devPkgDir);
166
+ fs.writeFileSync(
167
+ path.join(devPkgDir, "package.json"),
168
+ JSON.stringify({ name: "dev-pkg", version: "3.0.0" }),
169
+ );
170
+ fs.writeFileSync(path.join(devPkgDir, "index.d.ts"), "export const devVar: number;\n");
171
+
172
+ // Install other deps too
173
+ installTypedPkg();
174
+
175
+ const result = await indexLibraries({ projectRoot, libsDir, dbPath, includeDev: true });
176
+ // typed-pkg + dev-pkg (no-dts-pkg missing from node_modules so skipped silently)
177
+ expect(result.indexed).toBe(2);
178
+ expect(result.symbols).toBeGreaterThanOrEqual(4); // 3 from typed-pkg + 1 from dev-pkg
179
+ });
180
+
181
+ it("rebuildLibDbFromCards repopulates lib_symbols from cards", async () => {
182
+ installTypedPkg();
183
+ await indexLibraries({ projectRoot, libsDir, dbPath });
184
+
185
+ // Delete the DB and recreate from cards
186
+ const oldDb = openDb(dbPath);
187
+ oldDb.exec("DELETE FROM lib_symbols; DELETE FROM lib_symbols_fts; DELETE FROM libraries;");
188
+ oldDb.close();
189
+
190
+ rebuildLibDbFromCards(dbPath, libsDir);
191
+
192
+ const db = openDb(dbPath);
193
+ const symbols = searchLibSymbols(db, "", undefined, 10);
194
+ expect(symbols).toHaveLength(3);
195
+ db.close();
196
+ });
197
+
198
+ it("version bump prunes old cards+rows and adds new", async () => {
199
+ // Install v1
200
+ const pkgDir = path.join(nodeModulesDir, "typed-pkg");
201
+ fs.mkdirSync(path.join(pkgDir, "dist"), { recursive: true });
202
+ fs.writeFileSync(
203
+ path.join(pkgDir, "package.json"),
204
+ JSON.stringify({ name: "typed-pkg", version: "1.0.0", types: "dist/index.d.ts" }),
205
+ );
206
+ fs.writeFileSync(path.join(pkgDir, "dist", "index.d.ts"), "export const OLD: number;\n");
207
+
208
+ await indexLibraries({ projectRoot, libsDir, dbPath });
209
+
210
+ // Check v1 card exists
211
+ expect(fs.existsSync(path.join(libsDir, "typed-pkg@1.0.0"))).toBe(true);
212
+
213
+ // Now "upgrade" to v2
214
+ fs.writeFileSync(
215
+ path.join(pkgDir, "package.json"),
216
+ JSON.stringify({ name: "typed-pkg", version: "2.0.0", types: "dist/index.d.ts" }),
217
+ );
218
+ fs.writeFileSync(path.join(pkgDir, "dist", "index.d.ts"), "export const NEW: number;\n");
219
+
220
+ await indexLibraries({ projectRoot, libsDir, dbPath });
221
+
222
+ // Old card dir should be gone
223
+ expect(fs.existsSync(path.join(libsDir, "typed-pkg@1.0.0"))).toBe(false);
224
+
225
+ // New card dir exists
226
+ expect(fs.existsSync(path.join(libsDir, "typed-pkg@2.0.0"))).toBe(true);
227
+
228
+ // DB has new symbols only
229
+ const db = openDb(dbPath);
230
+ const symbols = searchLibSymbols(db, "", undefined, 10);
231
+ expect(symbols).toHaveLength(1);
232
+ expect(symbols[0]!.name).toBe("NEW");
233
+ expect(symbols[0]!.version).toBe("2.0.0");
234
+ db.close();
235
+ });
236
+ });