diff --git a/packages/argent/package.json b/packages/argent/package.json index c794d88f..280917a8 100644 --- a/packages/argent/package.json +++ b/packages/argent/package.json @@ -41,7 +41,9 @@ "scripts/postinstall.cjs" ], "dependencies": { - "@modelcontextprotocol/sdk": "^1.20.0" + "@modelcontextprotocol/sdk": "^1.20.0", + "tree-sitter": "^0.21.1", + "tree-sitter-typescript": "^0.23.2" }, "devDependencies": { "@argent/cli": "file:../argent-cli", diff --git a/packages/argent/scripts/bundle-tools.cjs b/packages/argent/scripts/bundle-tools.cjs index 8aa7609f..a847e2c0 100644 --- a/packages/argent/scripts/bundle-tools.cjs +++ b/packages/argent/scripts/bundle-tools.cjs @@ -119,6 +119,10 @@ esbuild.buildSync({ outfile: OUT_FILE, alias: ALIASES, mainFields: MAIN_FIELDS, + // tree-sitter / tree-sitter-typescript are native addons (.node) that esbuild + // cannot inline; keep them external so the bundle `require()`s them at runtime + // from the published package's own dependencies (see package.json). + external: ["tree-sitter", "tree-sitter-typescript"], }); console.log(`✓ Bundled tools server → ${path.relative(process.cwd(), OUT_FILE)}`); diff --git a/packages/tool-server/src/tools/profiler/react/react-profiler-component-source.ts b/packages/tool-server/src/tools/profiler/react/react-profiler-component-source.ts index aa2c6416..844995cd 100644 --- a/packages/tool-server/src/tools/profiler/react/react-profiler-component-source.ts +++ b/packages/tool-server/src/tools/profiler/react/react-profiler-component-source.ts @@ -23,10 +23,20 @@ Returns found: false if the component is not found in user-owned code (e.g. live const entry = astIndex.index.get(params.component_name); if (!entry) { + if (!astIndex.treeSitterAvailable) { + return { + found: false, + component: params.component_name, + message: + `Component source lookup is unavailable: the tree-sitter parser could not be loaded, ` + + `so no source files were indexed. Ensure @swmansion/argent's "tree-sitter" and ` + + `"tree-sitter-typescript" dependencies are installed.`, + }; + } return { found: false, component: params.component_name, - message: `Component "${params.component_name}" not found in ${params.project_root}`, + message: `Component "${params.component_name}" not found in ${params.project_root} (searched ${astIndex.indexedFiles} files).`, }; } diff --git a/packages/tool-server/test/react-profiler/ast-index.test.ts b/packages/tool-server/test/react-profiler/ast-index.test.ts new file mode 100644 index 00000000..10f26029 --- /dev/null +++ b/packages/tool-server/test/react-profiler/ast-index.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from "vitest"; +import { mkdtempSync, mkdirSync, writeFileSync } from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; +import { buildAstIndexWithDiagnostics } from "../../src/utils/react-profiler/pipeline/06-resolve/ast-index"; + +/** + * react-profiler-component-source returned found:false for every component in a + * standard TS/TSX Expo project because @swmansion/argent never declared (or + * shipped) tree-sitter / tree-sitter-typescript, so the parser require() threw, + * was swallowed, and the index was always empty. The grammar/match logic itself + * is correct — this test guards it for the idiomatic declaration forms and, via + * treeSitterAvailable, fails loudly if the parser dependency goes missing again. + */ +describe("buildAstIndexWithDiagnostics", () => { + it("indexes export-default-function, plain-function, and arrow TSX components", async () => { + const dir = mkdtempSync(join(tmpdir(), "ast-index-")); + mkdirSync(join(dir, "components"), { recursive: true }); + writeFileSync( + join(dir, "components", "foo.tsx"), + [ + `import React from "react";`, + `type P = { x: number };`, + `export default function Foo({ x }: P) { return {x}; }`, + `function Bar({ y }: { y: number }) { return {y}; }`, + `const Baz = ({ z }: { z: number }) => {z};`, + ``, + ].join("\n") + ); + + const res = await buildAstIndexWithDiagnostics(dir); + + expect(res.treeSitterAvailable).toBe(true); + expect(res.index.get("Foo")?.line).toBe(3); + expect(res.index.get("Bar")?.line).toBe(4); + expect(res.index.get("Baz")?.line).toBe(5); + }); +});