@treelocator/runtime 0.4.6 → 0.4.7

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.
@@ -20,8 +20,9 @@ export interface AncestryItem {
20
20
  export declare function collectAncestry(node: TreeNode): AncestryItem[];
21
21
  /**
22
22
  * Truncate ancestry to keep only the local context.
23
- * - If the clicked element has no filePath: keep up to the first ancestor with a file.
24
- * - If the clicked element has a filePath: keep up to the first ancestor with a DIFFERENT file.
23
+ * - If the clicked element has no file: keep up to the first ancestor with a file.
24
+ * - If the clicked element has a file: keep up to the first ancestor with a DIFFERENT file.
25
+ * Checks both client filePath and serverComponents for file info.
25
26
  * The ancestry array is bottom-up: index 0 = clicked element, last = root.
26
27
  */
27
28
  export declare function truncateAtFirstFile(items: AncestryItem[]): AncestryItem[];
@@ -122,25 +122,41 @@ function getInnermostNamedComponent(item) {
122
122
  return undefined;
123
123
  }
124
124
 
125
+ /**
126
+ * Get the effective file path from an AncestryItem, checking both
127
+ * client filePath and serverComponents (Next.js RSC, Phoenix, etc.).
128
+ */
129
+ function getItemFilePath(item) {
130
+ if (item.filePath) return item.filePath;
131
+ if (item.serverComponents && item.serverComponents.length > 0) {
132
+ const comp = item.serverComponents.find(sc => sc.type === "component");
133
+ if (comp) return comp.filePath;
134
+ return item.serverComponents[0].filePath;
135
+ }
136
+ return undefined;
137
+ }
138
+
125
139
  /**
126
140
  * Truncate ancestry to keep only the local context.
127
- * - If the clicked element has no filePath: keep up to the first ancestor with a file.
128
- * - If the clicked element has a filePath: keep up to the first ancestor with a DIFFERENT file.
141
+ * - If the clicked element has no file: keep up to the first ancestor with a file.
142
+ * - If the clicked element has a file: keep up to the first ancestor with a DIFFERENT file.
143
+ * Checks both client filePath and serverComponents for file info.
129
144
  * The ancestry array is bottom-up: index 0 = clicked element, last = root.
130
145
  */
131
146
  export function truncateAtFirstFile(items) {
132
147
  if (items.length === 0) return items;
133
- const clickedFile = items[0]?.filePath;
148
+ const clickedFile = getItemFilePath(items[0]);
134
149
  if (!clickedFile) {
135
150
  // Clicked element has no file: find first ancestor with any file
136
- const firstWithFile = items.findIndex(item => item.filePath);
151
+ const firstWithFile = items.findIndex(item => getItemFilePath(item));
137
152
  if (firstWithFile === -1) return items;
138
153
  return items.slice(0, firstWithFile + 1);
139
154
  }
140
155
 
141
156
  // Clicked element has a file: find first ancestor with a different file
142
157
  for (let i = 1; i < items.length; i++) {
143
- if (items[i].filePath && items[i].filePath !== clickedFile) {
158
+ const ancestorFile = getItemFilePath(items[i]);
159
+ if (ancestorFile && ancestorFile !== clickedFile) {
144
160
  return items.slice(0, i + 1);
145
161
  }
146
162
  }
@@ -367,6 +367,64 @@ describe("formatAncestryChain", () => {
367
367
  const result = truncateAtFirstFile(items);
368
368
  expect(result).toEqual(items);
369
369
  });
370
+ it("uses serverComponents file path when filePath is missing", () => {
371
+ const items = [{
372
+ elementName: "div",
373
+ componentName: "TurnActivityBox",
374
+ serverComponents: [{
375
+ name: "TurnActivityBox",
376
+ filePath: "components/MessageRow.tsx",
377
+ line: 921,
378
+ type: "component"
379
+ }]
380
+ }, {
381
+ elementName: "div",
382
+ componentName: "MessageRow",
383
+ serverComponents: [{
384
+ name: "MessageRow",
385
+ filePath: "components/chat/ChatViewport.tsx",
386
+ line: 917,
387
+ type: "component"
388
+ }]
389
+ }, {
390
+ elementName: "main",
391
+ componentName: "Home",
392
+ serverComponents: [{
393
+ name: "Home",
394
+ filePath: "app/page.tsx",
395
+ line: 817,
396
+ type: "component"
397
+ }]
398
+ }];
399
+ const result = truncateAtFirstFile(items);
400
+ expect(result).toEqual([items[0], items[1]]);
401
+ });
402
+ it("uses serverComponents when clicked element has no filePath but has serverComponents", () => {
403
+ const items = [{
404
+ elementName: "span",
405
+ componentName: "Button"
406
+ }, {
407
+ elementName: "div",
408
+ componentName: "Card",
409
+ serverComponents: [{
410
+ name: "Card",
411
+ filePath: "src/Card.tsx",
412
+ line: 10,
413
+ type: "component"
414
+ }]
415
+ }, {
416
+ elementName: "div",
417
+ componentName: "App",
418
+ serverComponents: [{
419
+ name: "App",
420
+ filePath: "src/App.tsx",
421
+ line: 1,
422
+ type: "component"
423
+ }]
424
+ }];
425
+ const result = truncateAtFirstFile(items);
426
+ expect(result).toEqual([items[0], items[1]]);
427
+ });
370
428
  it("returns empty array for empty input", () => {
371
429
  expect(truncateAtFirstFile([])).toEqual([]);
372
430
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treelocator/runtime",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "TreeLocatorJS runtime for component ancestry tracking. Alt+click any element to copy its component tree to clipboard. Exposes window.__treelocator__ API for browser automation (Playwright, Puppeteer, Selenium, Cypress).",
5
5
  "keywords": [
6
6
  "locator",
@@ -336,6 +336,34 @@ describe("formatAncestryChain", () => {
336
336
  expect(result).toEqual(items);
337
337
  });
338
338
 
339
+ it("uses serverComponents file path when filePath is missing", () => {
340
+ const items: AncestryItem[] = [
341
+ { elementName: "div", componentName: "TurnActivityBox", serverComponents: [{ name: "TurnActivityBox", filePath: "components/MessageRow.tsx", line: 921, type: "component" }] },
342
+ { elementName: "div", componentName: "MessageRow", serverComponents: [{ name: "MessageRow", filePath: "components/chat/ChatViewport.tsx", line: 917, type: "component" }] },
343
+ { elementName: "main", componentName: "Home", serverComponents: [{ name: "Home", filePath: "app/page.tsx", line: 817, type: "component" }] },
344
+ ];
345
+
346
+ const result = truncateAtFirstFile(items);
347
+ expect(result).toEqual([
348
+ items[0],
349
+ items[1],
350
+ ]);
351
+ });
352
+
353
+ it("uses serverComponents when clicked element has no filePath but has serverComponents", () => {
354
+ const items: AncestryItem[] = [
355
+ { elementName: "span", componentName: "Button" },
356
+ { elementName: "div", componentName: "Card", serverComponents: [{ name: "Card", filePath: "src/Card.tsx", line: 10, type: "component" }] },
357
+ { elementName: "div", componentName: "App", serverComponents: [{ name: "App", filePath: "src/App.tsx", line: 1, type: "component" }] },
358
+ ];
359
+
360
+ const result = truncateAtFirstFile(items);
361
+ expect(result).toEqual([
362
+ items[0],
363
+ items[1],
364
+ ]);
365
+ });
366
+
339
367
  it("returns empty array for empty input", () => {
340
368
  expect(truncateAtFirstFile([])).toEqual([]);
341
369
  });
@@ -164,27 +164,43 @@ function getInnermostNamedComponent(item: AncestryItem | null | undefined): stri
164
164
  return undefined;
165
165
  }
166
166
 
167
+ /**
168
+ * Get the effective file path from an AncestryItem, checking both
169
+ * client filePath and serverComponents (Next.js RSC, Phoenix, etc.).
170
+ */
171
+ function getItemFilePath(item: AncestryItem): string | undefined {
172
+ if (item.filePath) return item.filePath;
173
+ if (item.serverComponents && item.serverComponents.length > 0) {
174
+ const comp = item.serverComponents.find((sc) => sc.type === "component");
175
+ if (comp) return comp.filePath;
176
+ return item.serverComponents[0]!.filePath;
177
+ }
178
+ return undefined;
179
+ }
180
+
167
181
  /**
168
182
  * Truncate ancestry to keep only the local context.
169
- * - If the clicked element has no filePath: keep up to the first ancestor with a file.
170
- * - If the clicked element has a filePath: keep up to the first ancestor with a DIFFERENT file.
183
+ * - If the clicked element has no file: keep up to the first ancestor with a file.
184
+ * - If the clicked element has a file: keep up to the first ancestor with a DIFFERENT file.
185
+ * Checks both client filePath and serverComponents for file info.
171
186
  * The ancestry array is bottom-up: index 0 = clicked element, last = root.
172
187
  */
173
188
  export function truncateAtFirstFile(items: AncestryItem[]): AncestryItem[] {
174
189
  if (items.length === 0) return items;
175
190
 
176
- const clickedFile = items[0]?.filePath;
191
+ const clickedFile = getItemFilePath(items[0]!);
177
192
 
178
193
  if (!clickedFile) {
179
194
  // Clicked element has no file: find first ancestor with any file
180
- const firstWithFile = items.findIndex((item) => item.filePath);
195
+ const firstWithFile = items.findIndex((item) => getItemFilePath(item));
181
196
  if (firstWithFile === -1) return items;
182
197
  return items.slice(0, firstWithFile + 1);
183
198
  }
184
199
 
185
200
  // Clicked element has a file: find first ancestor with a different file
186
201
  for (let i = 1; i < items.length; i++) {
187
- if (items[i]!.filePath && items[i]!.filePath !== clickedFile) {
202
+ const ancestorFile = getItemFilePath(items[i]!);
203
+ if (ancestorFile && ancestorFile !== clickedFile) {
188
204
  return items.slice(0, i + 1);
189
205
  }
190
206
  }