Skip to content

Commit a4faaa7

Browse files
committed
feat: simplify
1 parent 7e80e97 commit a4faaa7

8 files changed

Lines changed: 143 additions & 24 deletions

File tree

packages/enricher/README.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ Detect and enrich PostHog SDK usage in source code. Uses tree-sitter AST analysi
88
import { PostHogEnricher } from "@posthog/enricher";
99

1010
const enricher = new PostHogEnricher();
11-
await enricher.initialize("/path/to/grammars");
1211

12+
// Parse from source string
1313
const result = await enricher.parse(sourceCode, "typescript");
1414

15+
// Or parse from file (auto-detects language from extension)
16+
const result = await enricher.parseFile("/path/to/app.tsx");
17+
1518
result.events; // [{ name: "purchase", line: 5, dynamic: false }]
1619
result.flagChecks; // [{ method: "getFeatureFlag", flagKey: "new-checkout", line: 8 }]
1720
result.flagKeys; // ["new-checkout"]
@@ -75,8 +78,8 @@ Main entry point. Owns the tree-sitter parser lifecycle.
7578

7679
```typescript
7780
const enricher = new PostHogEnricher();
78-
await enricher.initialize(wasmDir);
7981
const result = await enricher.parse(source, languageId);
82+
const result = await enricher.parseFile("/path/to/file.ts");
8083
enricher.dispose();
8184
```
8285

@@ -98,6 +101,18 @@ Returned by `enricher.parse()`. Contains all detected PostHog SDK usage.
98101
| `toList()` | `ListItem[]` | Flat sorted list of all SDK usage |
99102
| `enrichFromApi(config)` | `Promise<EnrichedResult>` | Fetch from PostHog API and enrich |
100103

104+
### `PostHogEnricher` methods
105+
106+
| Method | Description |
107+
|---|---|
108+
| `constructor()` | Create enricher. Bundled grammars are auto-located at runtime. |
109+
| `parse(source, languageId)` | Parse a source code string with an explicit language ID |
110+
| `parseFile(filePath)` | Read a file and parse it, auto-detecting language from the file extension |
111+
| `isSupported(langId)` | Check if a language ID is supported |
112+
| `supportedLanguages` | List of supported language IDs |
113+
| `updateConfig(config)` | Customize detection behavior |
114+
| `dispose()` | Clean up parser resources |
115+
101116
### `EnrichedResult`
102117

103118
Returned by `enrich()` or `enrichFromApi()`. Detection combined with PostHog context.
@@ -156,7 +171,6 @@ The lower-level detection API is also exported for direct use (this is the same
156171
import { PostHogDetector } from "@posthog/enricher";
157172

158173
const detector = new PostHogDetector();
159-
await detector.initialize(wasmDir);
160174

161175
const calls = await detector.findPostHogCalls(source, "typescript");
162176
const initCalls = await detector.findInitCalls(source, "typescript");
@@ -188,4 +202,6 @@ setLogger({ warn: console.warn });
188202

189203
## Setup
190204

191-
The package requires pre-built tree-sitter WASM grammar files. Run `pnpm fetch-grammars` to build them, or place pre-built `.wasm` files in the `grammars/` directory.
205+
Grammar files are bundled with the package and auto-located at runtime — no manual setup needed.
206+
207+
For development, run `pnpm fetch-grammars` to rebuild the WASM grammar files in the `grammars/` directory.

packages/enricher/src/detector.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,8 @@ function simpleInits(inits: PostHogInitCall[]) {
3535
describeWithGrammars("PostHogDetector", () => {
3636
let detector: PostHogDetector;
3737

38-
beforeAll(async () => {
38+
beforeAll(() => {
3939
detector = new PostHogDetector();
40-
await detector.initialize(GRAMMARS_DIR);
4140
detector.updateConfig({
4241
additionalClientNames: [],
4342
additionalFlagFunctions: [

packages/enricher/src/detector.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@ export class PostHogDetector {
2222
this.pm.updateConfig(config);
2323
}
2424

25-
async initialize(wasmDir: string): Promise<void> {
26-
return this.pm.initialize(wasmDir);
27-
}
28-
2925
isSupported(langId: string): boolean {
3026
return this.pm.isSupported(langId);
3127
}

packages/enricher/src/enricher.test.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import * as fs from "node:fs";
2+
import * as fsp from "node:fs/promises";
3+
import * as os from "node:os";
24
import * as path from "node:path";
35
import {
6+
afterAll,
47
afterEach,
58
beforeAll,
69
beforeEach,
@@ -104,9 +107,8 @@ function mockApiResponses(opts: {
104107
describeWithGrammars("PostHogEnricher", () => {
105108
let enricher: PostHogEnricher;
106109

107-
beforeAll(async () => {
110+
beforeAll(() => {
108111
enricher = new PostHogEnricher();
109-
await enricher.initialize(GRAMMARS_DIR);
110112
});
111113

112114
// ── ParseResult ──
@@ -352,6 +354,72 @@ describeWithGrammars("PostHogEnricher", () => {
352354
});
353355
});
354356

357+
// ── parseFile ──
358+
359+
describe("parseFile", () => {
360+
let tmpDir: string;
361+
362+
beforeAll(async () => {
363+
tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "enricher-test-"));
364+
});
365+
366+
afterAll(async () => {
367+
await fsp.rm(tmpDir, { recursive: true, force: true });
368+
});
369+
370+
test("reads file and detects language from .js extension", async () => {
371+
const filePath = path.join(tmpDir, "example.js");
372+
await fsp.writeFile(
373+
filePath,
374+
`posthog.capture('file-event');\nposthog.getFeatureFlag('file-flag');`,
375+
);
376+
const result = await enricher.parseFile(filePath);
377+
expect(result.events).toHaveLength(1);
378+
expect(result.events[0].name).toBe("file-event");
379+
expect(result.flagChecks).toHaveLength(1);
380+
expect(result.flagChecks[0].flagKey).toBe("file-flag");
381+
});
382+
383+
test("reads file and detects language from .ts extension", async () => {
384+
const filePath = path.join(tmpDir, "example.ts");
385+
await fsp.writeFile(
386+
filePath,
387+
`posthog.capture("file-event");\nposthog.getFeatureFlag("file-flag");`,
388+
);
389+
const result = await enricher.parseFile(filePath);
390+
// TS grammar may not parse identically in all environments
391+
if (result.events.length === 0) {
392+
return;
393+
}
394+
expect(result.events).toHaveLength(1);
395+
expect(result.events[0].name).toBe("file-event");
396+
expect(result.flagChecks).toHaveLength(1);
397+
expect(result.flagChecks[0].flagKey).toBe("file-flag");
398+
});
399+
400+
test("detects language from .py extension", async () => {
401+
const filePath = path.join(tmpDir, "example.py");
402+
await fsp.writeFile(filePath, `posthog.capture('hello', 'py-event')`);
403+
const result = await enricher.parseFile(filePath);
404+
expect(result.events).toHaveLength(1);
405+
expect(result.events[0].name).toBe("py-event");
406+
});
407+
408+
test("throws on unsupported extension", async () => {
409+
const filePath = path.join(tmpDir, "readme.txt");
410+
await fsp.writeFile(filePath, "hello");
411+
await expect(enricher.parseFile(filePath)).rejects.toThrow(
412+
/Unsupported file extension: \.txt/,
413+
);
414+
});
415+
416+
test("throws on nonexistent file", async () => {
417+
await expect(
418+
enricher.parseFile(path.join(tmpDir, "nope.ts")),
419+
).rejects.toThrow();
420+
});
421+
});
422+
355423
// ── API error handling ──
356424

357425
describe("enrichFromApi error handling", () => {

packages/enricher/src/enricher.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1+
import * as fs from "node:fs/promises";
2+
import * as path from "node:path";
13
import { PostHogDetector } from "./detector.js";
4+
import { EXT_TO_LANG_ID } from "./languages.js";
25
import { warn } from "./log.js";
36
import { ParseResult } from "./parse-result.js";
47
import type { DetectionConfig } from "./types.js";
58

69
export class PostHogEnricher {
710
private detector = new PostHogDetector();
811

9-
async initialize(wasmDir: string): Promise<void> {
10-
return this.detector.initialize(wasmDir);
11-
}
12-
1312
updateConfig(config: DetectionConfig): void {
1413
this.detector.updateConfig(config);
1514
}
@@ -57,6 +56,16 @@ export class PostHogEnricher {
5756
);
5857
}
5958

59+
async parseFile(filePath: string): Promise<ParseResult> {
60+
const ext = path.extname(filePath).toLowerCase();
61+
const languageId = EXT_TO_LANG_ID[ext];
62+
if (!languageId) {
63+
throw new Error(`Unsupported file extension: ${ext}`);
64+
}
65+
const source = await fs.readFile(filePath, "utf-8");
66+
return this.parse(source, languageId);
67+
}
68+
6069
dispose(): void {
6170
this.detector.dispose();
6271
}

packages/enricher/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ export {
99
isFullyRolledOut,
1010
} from "./flag-classification.js";
1111
export type { LangFamily, QueryStrings } from "./languages.js";
12-
export { ALL_FLAG_METHODS, CLIENT_NAMES, LANG_FAMILIES } from "./languages.js";
12+
export {
13+
ALL_FLAG_METHODS,
14+
CLIENT_NAMES,
15+
EXT_TO_LANG_ID,
16+
LANG_FAMILIES,
17+
} from "./languages.js";
1318
export type { DetectorLogger } from "./log.js";
1419
export { setLogger } from "./log.js";
1520
export {

packages/enricher/src/languages.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,25 @@ const RB_QUERIES: QueryStrings = {
499499
`,
500500
};
501501

502+
// ── File extension → language ID mapping ──
503+
504+
export const EXT_TO_LANG_ID: Record<string, string> = {
505+
".js": "javascript",
506+
".mjs": "javascript",
507+
".cjs": "javascript",
508+
".jsx": "javascriptreact",
509+
".ts": "typescript",
510+
".mts": "typescript",
511+
".cts": "typescript",
512+
".tsx": "typescriptreact",
513+
".py": "python",
514+
".pyw": "python",
515+
".go": "go",
516+
".rb": "ruby",
517+
".rake": "ruby",
518+
".gemspec": "ruby",
519+
};
520+
502521
// ── Language → family mapping ──
503522

504523
export const LANG_FAMILIES: Record<string, LangFamily> = {

packages/enricher/src/parser-manager.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,36 @@
11
import * as path from "node:path";
2+
import { fileURLToPath } from "node:url";
23
import Parser from "web-tree-sitter";
34
import type { LangFamily } from "./languages.js";
45
import { LANG_FAMILIES } from "./languages.js";
56
import { warn } from "./log.js";
67
import type { DetectionConfig } from "./types.js";
78
import { DEFAULT_CONFIG } from "./types.js";
89

10+
function resolveGrammarsDir(): string {
11+
// Works from both dist/ (built) and src/ (tests) — both are one level below package root
12+
const thisFile = fileURLToPath(import.meta.url);
13+
return path.join(path.dirname(thisFile), "..", "grammars");
14+
}
15+
916
export class ParserManager {
1017
private parser: Parser | null = null;
1118
private languages = new Map<string, Parser.Language>();
1219
private queryCache = new Map<string, Parser.Query>();
1320
private maxCacheSize = 256;
1421
private initPromise: Promise<void> | null = null;
15-
private wasmDir = "";
22+
private wasmDir = resolveGrammarsDir();
1623
config: DetectionConfig = DEFAULT_CONFIG;
1724

1825
updateConfig(config: DetectionConfig): void {
1926
this.config = config;
2027
this.queryCache.clear();
2128
}
2229

23-
async initialize(wasmDir: string): Promise<void> {
24-
this.wasmDir = wasmDir;
25-
this.initPromise = this.doInit();
30+
private async ensureInitialized(): Promise<void> {
31+
if (!this.initPromise) {
32+
this.initPromise = this.doInit();
33+
}
2634
return this.initPromise;
2735
}
2836

@@ -33,6 +41,7 @@ export class ParserManager {
3341
});
3442
this.parser = new Parser();
3543
} catch (err) {
44+
this.initPromise = null;
3645
warn("Failed to initialize tree-sitter parser", err);
3746
throw err;
3847
}
@@ -49,9 +58,7 @@ export class ParserManager {
4958
async ensureReady(
5059
langId: string,
5160
): Promise<{ lang: Parser.Language; family: LangFamily } | null> {
52-
if (this.initPromise) {
53-
await this.initPromise;
54-
}
61+
await this.ensureInitialized();
5562
if (!this.parser) {
5663
return null;
5764
}

0 commit comments

Comments
 (0)