Skip to content

Commit a73fd32

Browse files
committed
perf: skip unused Node mapper context
Keep contributor's lazy-context direction while gating Node-family mappers behind bounded project signals and preserving package-less workspace discovery.\n\nCo-authored-by: Tanmay-008 <tanmayshirbhayye60@gmail.com>
1 parent ec1b12a commit a73fd32

11 files changed

Lines changed: 338 additions & 104 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 0.7.1 - Unreleased
44

5+
- Improved feature mapping so pure non-Node repositories skip Node workspace and Turbo discovery while Node consumers share one lazy context, thanks @Tanmay-008.
56
- Fixed revalidation prompts to compact historical and feature metadata and hard-cap metadata lists even when configured file limits are high, preventing provider input overflows, thanks @pai-scaffolde.
67
- Added an opt-in Claude host auth context that preserves the default-deny environment, uses Claude Code safe mode, validates auth through doctor, and reports redacted OAuth failure signals, thanks @grantjayy.
78

src/mapper.test.ts

Lines changed: 137 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import { mkdir, symlink } from "node:fs/promises";
22
import { basename, join } from "node:path";
3-
import { describe, expect, it, vi } from "vitest";
3+
import { afterEach, describe, expect, it, vi } from "vitest";
44
import { detectProject } from "./detect.js";
55
import { mapFeatures } from "./mapper.js";
66
import * as projectsModule from "./mappers/projects.js";
77
import { discoverNodeProjects, scriptCommand } from "./mappers/projects.js";
8+
import * as turboModule from "./mappers/turbo.js";
89
import { turboTaskGraph } from "./mappers/turbo.js";
910
import { fixtureRoot, writeFixture } from "./test-helpers.js";
1011

1112
const symlinkIt = process.platform === "win32" ? it.skip : it;
1213

14+
afterEach(() => {
15+
vi.restoreAllMocks();
16+
});
17+
1318
describe("mapFeatures", () => {
1419
it("quotes dynamic Node validation command parts", () => {
1520
expect(scriptCommand("pnpm", "packages/app; touch INJECTED", "test")).toBe(
@@ -1121,13 +1126,17 @@ describe("mapFeatures", () => {
11211126
);
11221127
await writeFixture(root, "apps/web/src/pages/HomePage.test.tsx", "test('home', () => {});\n");
11231128

1129+
const projectsSpy = vi.spyOn(projectsModule, "discoverNodeProjects");
1130+
const taskGraphSpy = vi.spyOn(turboModule, "turboTaskGraph");
11241131
const project = await detectProject(root);
11251132
const result = await mapFeatures(root, project, []);
11261133
const route = result.features.find((feature) => feature.title === "React route /home");
11271134

11281135
expect(route?.tests).toEqual([
11291136
{ path: "apps/web/src/pages/HomePage.test.tsx", command: "pnpm turbo run test --filter web" },
11301137
]);
1138+
expect(projectsSpy).toHaveBeenCalledTimes(1);
1139+
expect(taskGraphSpy).toHaveBeenCalledTimes(1);
11311140
});
11321141

11331142
it("suppresses fallback validation commands for persistent Turbo tasks", async () => {
@@ -12924,19 +12933,19 @@ exclude = ["packages/legacy"]
1292412933
root,
1292512934
"routes/web.php",
1292612935
"<?php\n" +
12927-
"use App\\Http\\Controllers\\LandingPageController;\n" +
12928-
"use App\\Http\\Controllers\\TrackController;\n" +
12929-
"Route::get('/', LandingPageController::class);\n" +
12930-
"Route::post('/tracks', [TrackController::class, 'store']);\n" +
12931-
"Route::resource('catalog', TrackController::class);\n",
12936+
"use App\\Http\\Controllers\\LandingPageController;\n" +
12937+
"use App\\Http\\Controllers\\TrackController;\n" +
12938+
"Route::get('/', LandingPageController::class);\n" +
12939+
"Route::post('/tracks', [TrackController::class, 'store']);\n" +
12940+
"Route::resource('catalog', TrackController::class);\n",
1293212941
);
1293312942
await writeFixture(
1293412943
root,
1293512944
"app/Http/Controllers/TrackController.php",
1293612945
"<?php\nnamespace App\\Http\\Controllers;\n" +
12937-
"use App\\Http\\Requests\\StoreTrackRequest;\n" +
12938-
"use App\\Services\\TrackUploadService;\n" +
12939-
"final class TrackController {}\n",
12946+
"use App\\Http\\Requests\\StoreTrackRequest;\n" +
12947+
"use App\\Services\\TrackUploadService;\n" +
12948+
"final class TrackController {}\n",
1294012949
);
1294112950
await writeFixture(
1294212951
root,
@@ -13056,15 +13065,15 @@ exclude = ["packages/legacy"]
1305613065
root,
1305713066
"routes/admin.php",
1305813067
"<?php\n" +
13059-
"use App\\Http\\Controllers\\Admin\\{UserController};\n" +
13060-
"Route::prefix('admin')->middleware('auth')->get('/users', UserController::class);\n",
13068+
"use App\\Http\\Controllers\\Admin\\{UserController};\n" +
13069+
"Route::prefix('admin')->middleware('auth')->get('/users', UserController::class);\n",
1306113070
);
1306213071
await writeFixture(
1306313072
root,
1306413073
"routes/api.php",
1306513074
"<?php\n" +
13066-
"use App\\Http\\Controllers\\Api\\UserController;\n" +
13067-
"Route::get('/users', UserController::class);\n",
13075+
"use App\\Http\\Controllers\\Api\\UserController;\n" +
13076+
"Route::get('/users', UserController::class);\n",
1306813077
);
1306913078
await writeFixture(
1307013079
root,
@@ -13127,8 +13136,8 @@ exclude = ["packages/legacy"]
1312713136
root,
1312813137
"routes/web.php",
1312913138
"<?php\n" +
13130-
"Route::get('/qualified', \\App\\Http\\Controllers\\QualifiedController::class);\n" +
13131-
"Route::post('/qualified-array', [App\\Http\\Controllers\\ArrayQualifiedController::class, 'store']);\n",
13139+
"Route::get('/qualified', \\App\\Http\\Controllers\\QualifiedController::class);\n" +
13140+
"Route::post('/qualified-array', [App\\Http\\Controllers\\ArrayQualifiedController::class, 'store']);\n",
1313213141
);
1313313142
await writeFixture(
1313413143
root,
@@ -13184,10 +13193,10 @@ exclude = ["packages/legacy"]
1318413193
root,
1318513194
"routes/web.php",
1318613195
"<?php\n" +
13187-
"use App\\Http\\Controllers\\Admin\\UserController as AdminUserController;\n" +
13188-
"use App\\Http\\Controllers\\Api\\UserController as ApiUserController;\n" +
13189-
"Route::get('/admin/users', AdminUserController::class);\n" +
13190-
"Route::get('/api/users', ApiUserController::class);\n",
13196+
"use App\\Http\\Controllers\\Admin\\UserController as AdminUserController;\n" +
13197+
"use App\\Http\\Controllers\\Api\\UserController as ApiUserController;\n" +
13198+
"Route::get('/admin/users', AdminUserController::class);\n" +
13199+
"Route::get('/api/users', ApiUserController::class);\n",
1319113200
);
1319213201
await writeFixture(
1319313202
root,
@@ -13234,8 +13243,8 @@ exclude = ["packages/legacy"]
1323413243
root,
1323513244
"routes/web.php",
1323613245
"<?php\n" +
13237-
"use App\\Http\\Controllers\\Api;\n" +
13238-
"Route::get('/api/users', Api\\UserController::class);\n",
13246+
"use App\\Http\\Controllers\\Api;\n" +
13247+
"Route::get('/api/users', Api\\UserController::class);\n",
1323913248
);
1324013249
await writeFixture(
1324113250
root,
@@ -13277,8 +13286,8 @@ exclude = ["packages/legacy"]
1327713286
root,
1327813287
"routes/web.php",
1327913288
"<?php\n" +
13280-
"use App\\Http\\Controllers\\DashboardController;\n" +
13281-
"Route::prefix('{tenant}')->get('/dashboard', DashboardController::class);\n",
13289+
"use App\\Http\\Controllers\\DashboardController;\n" +
13290+
"Route::prefix('{tenant}')->get('/dashboard', DashboardController::class);\n",
1328213291
);
1328313292
await writeFixture(
1328413293
root,
@@ -13316,10 +13325,10 @@ exclude = ["packages/legacy"]
1331613325
root,
1331713326
"routes/web.php",
1331813327
"<?php\n" +
13319-
"use App\\Http\\Controllers\\UserController;\n" +
13320-
'Route::group(["prefix" => "admin"], function () {\n' +
13321-
' Route::get("/users", UserController::class);\n' +
13322-
"});\n",
13328+
"use App\\Http\\Controllers\\UserController;\n" +
13329+
'Route::group(["prefix" => "admin"], function () {\n' +
13330+
' Route::get("/users", UserController::class);\n' +
13331+
"});\n",
1332313332
);
1332413333
await writeFixture(
1332513334
root,
@@ -13362,12 +13371,12 @@ exclude = ["packages/legacy"]
1336213371
root,
1336313372
"routes/web.php",
1336413373
"<?php\n" +
13365-
"use App\\Http\\Controllers\\UserController;\n" +
13366-
"Route::group(['prefix' => 'admin'], function () {\n" +
13367-
" Route::controller(UserController::class)->group(function () {\n" +
13368-
" Route::get('/users', 'index');\n" +
13369-
" });\n" +
13370-
"});\n",
13374+
"use App\\Http\\Controllers\\UserController;\n" +
13375+
"Route::group(['prefix' => 'admin'], function () {\n" +
13376+
" Route::controller(UserController::class)->group(function () {\n" +
13377+
" Route::get('/users', 'index');\n" +
13378+
" });\n" +
13379+
"});\n",
1337113380
);
1337213381
await writeFixture(
1337313382
root,
@@ -13406,12 +13415,12 @@ exclude = ["packages/legacy"]
1340613415
root,
1340713416
"routes/web.php",
1340813417
"<?php\n" +
13409-
"use App\\Http\\Controllers\\UserController;\n" +
13410-
"Route::group(['middleware' => 'auth'], function () {\n" +
13411-
" Route::group(['prefix' => 'admin'], function () {\n" +
13412-
" Route::get('/users', UserController::class);\n" +
13413-
" });\n" +
13414-
"});\n",
13418+
"use App\\Http\\Controllers\\UserController;\n" +
13419+
"Route::group(['middleware' => 'auth'], function () {\n" +
13420+
" Route::group(['prefix' => 'admin'], function () {\n" +
13421+
" Route::get('/users', UserController::class);\n" +
13422+
" });\n" +
13423+
"});\n",
1341513424
);
1341613425
await writeFixture(
1341713426
root,
@@ -13450,11 +13459,11 @@ exclude = ["packages/legacy"]
1345013459
root,
1345113460
"routes/web.php",
1345213461
"<?php\n" +
13453-
"use App\\Http\\Controllers\\UserController;\n" +
13454-
"Route::prefix('admin')->controller(UserController::class)->group(function () {\n" +
13455-
" Route::get('/users', 'index');\n" +
13456-
" Route::post('/users', 'store');\n" +
13457-
"});\n",
13462+
"use App\\Http\\Controllers\\UserController;\n" +
13463+
"Route::prefix('admin')->controller(UserController::class)->group(function () {\n" +
13464+
" Route::get('/users', 'index');\n" +
13465+
" Route::post('/users', 'store');\n" +
13466+
"});\n",
1345813467
);
1345913468
await writeFixture(
1346013469
root,
@@ -13503,9 +13512,9 @@ exclude = ["packages/legacy"]
1350313512
root,
1350413513
"routes/web.php",
1350513514
"<?php\n" +
13506-
"use App\\Http\\Controllers\\TrackController;\n" +
13507-
"Route::get('/tracks', TrackController::class);\n" +
13508-
"Route::post('/tracks', [TrackController::class, 'store']);\n",
13515+
"use App\\Http\\Controllers\\TrackController;\n" +
13516+
"Route::get('/tracks', TrackController::class);\n" +
13517+
"Route::post('/tracks', [TrackController::class, 'store']);\n",
1350913518
);
1351013519

1351113520
const project = await detectProject(root);
@@ -13517,10 +13526,10 @@ exclude = ["packages/legacy"]
1351713526
root,
1351813527
"routes/web.php",
1351913528
"<?php\n" +
13520-
"use App\\Http\\Controllers\\TrackController;\n" +
13521-
"Route::get('/catalog/tracks', TrackController::class);\n" +
13522-
"Route::get('/tracks', TrackController::class);\n" +
13523-
"Route::post('/tracks', [TrackController::class, 'store']);\n",
13529+
"use App\\Http\\Controllers\\TrackController;\n" +
13530+
"Route::get('/catalog/tracks', TrackController::class);\n" +
13531+
"Route::get('/tracks', TrackController::class);\n" +
13532+
"Route::post('/tracks', [TrackController::class, 'store']);\n",
1352413533
);
1352513534

1352613535
const second = await mapFeatures(root, project, []);
@@ -13554,10 +13563,10 @@ exclude = ["packages/legacy"]
1355413563
root,
1355513564
"routes/web.php",
1355613565
"<?php\n" +
13557-
"use App\\Http\\Controllers\\ArchiveController;\n" +
13558-
"// Route::get('/old', ArchiveController::class);\n" +
13559-
"/*\nRoute::get('/blocked', ArchiveController::class);\n*/\n" +
13560-
"Route::get('/current', ArchiveController::class);\n",
13566+
"use App\\Http\\Controllers\\ArchiveController;\n" +
13567+
"// Route::get('/old', ArchiveController::class);\n" +
13568+
"/*\nRoute::get('/blocked', ArchiveController::class);\n*/\n" +
13569+
"Route::get('/current', ArchiveController::class);\n",
1356113570
);
1356213571
await writeFixture(
1356313572
root,
@@ -13599,9 +13608,9 @@ exclude = ["packages/legacy"]
1359913608
root,
1360013609
"app/Console/Commands/SyncCatalog.php",
1360113610
"<?php\nnamespace App\\Console\\Commands;\n" +
13602-
"// protected $signature = 'app:old-sync';\n" +
13603-
"/* #[Signature('app:blocked-sync')] */\n" +
13604-
"final class SyncCatalog { protected $signature = 'app:sync-catalog {--force}'; }\n",
13611+
"// protected $signature = 'app:old-sync';\n" +
13612+
"/* #[Signature('app:blocked-sync')] */\n" +
13613+
"final class SyncCatalog { protected $signature = 'app:sync-catalog {--force}'; }\n",
1360513614
);
1360613615

1360713616
const project = await detectProject(root);
@@ -17191,16 +17200,81 @@ EndProject
1719117200
expect(ownedFiles).not.toContain("testdata/Example/Example.cs");
1719217201
});
1719317202

17194-
it("skips Node.js project discovery I/O for pure Go projects", async () => {
17195-
const root = await fixtureRoot("clawpatch-map-node-coupling-");
17203+
it.each([
17204+
{
17205+
name: "Go",
17206+
manifest: ["go.mod", "module example.com/go-app\n"],
17207+
source: ["main.go", "package main\nfunc main() {}\n"],
17208+
},
17209+
{
17210+
name: "Python",
17211+
manifest: ["pyproject.toml", "[project]\nname = 'python-app'\nversion = '1.0.0'\n"],
17212+
source: ["src/app.py", "def main(): pass\n"],
17213+
},
17214+
] as const)(
17215+
"skips Node project and Turbo I/O for pure $name projects",
17216+
async ({ manifest, source }) => {
17217+
const root = await fixtureRoot("clawpatch-map-node-coupling-");
17218+
await writeFixture(root, manifest[0], manifest[1]);
17219+
await writeFixture(root, source[0], source[1]);
17220+
const projectsSpy = vi.spyOn(projectsModule, "discoverNodeProjects");
17221+
const taskGraphSpy = vi.spyOn(turboModule, "turboTaskGraph");
17222+
17223+
const project = await detectProject(root);
17224+
await mapFeatures(root, project, []);
17225+
17226+
expect(projectsSpy).not.toHaveBeenCalled();
17227+
expect(taskGraphSpy).not.toHaveBeenCalled();
17228+
},
17229+
);
17230+
17231+
it("does not block non-Node mappers on the fallback Node signal", async () => {
17232+
const root = await fixtureRoot("clawpatch-map-node-signal-");
1719617233
await writeFixture(root, "go.mod", "module example.com/go-app\n");
1719717234
await writeFixture(root, "main.go", "package main\nfunc main() {}\n");
17235+
const signal = Promise.withResolvers<boolean>();
17236+
vi.spyOn(projectsModule, "hasFallbackNodeProjectSignal").mockReturnValue(signal.promise);
17237+
const projectsSpy = vi.spyOn(projectsModule, "discoverNodeProjects");
17238+
const goDone = Promise.withResolvers<void>();
17239+
const project = await detectProject(root);
17240+
17241+
const mapping = mapFeatures(root, project, [], {
17242+
onProgress(event) {
17243+
if (event.event === "mapper-done" && event.mapper === "go") {
17244+
goDone.resolve();
17245+
}
17246+
},
17247+
});
17248+
await goDone.promise;
17249+
17250+
expect(projectsSpy).not.toHaveBeenCalled();
17251+
signal.resolve(false);
17252+
await expect(mapping).resolves.toBeDefined();
17253+
});
1719817254

17199-
const spy = vi.spyOn(projectsModule, "discoverNodeProjects");
17255+
it("preserves package-less Node projects under conventional roots", async () => {
17256+
const root = await fixtureRoot("clawpatch-map-package-less-node-");
17257+
await writeFixture(root, "apps/web/src/index.ts", "export function start() {}\n");
1720017258

1720117259
const project = await detectProject(root);
17202-
await mapFeatures(root, project, []);
17260+
const result = await mapFeatures(root, project, []);
17261+
17262+
expect(project.detected.languages).not.toContain("typescript");
17263+
expect(result.features.map((feature) => feature.title)).toContain("Node source apps/web/src");
17264+
});
17265+
17266+
it("propagates Turbo failures and retries with a fresh mapping context", async () => {
17267+
const root = await fixtureRoot("clawpatch-map-turbo-failure-");
17268+
await writeFixture(root, "package.json", JSON.stringify({ name: "node-app" }));
17269+
await writeFixture(root, "src/index.ts", "export const value = true;\n");
17270+
await writeFixture(root, "turbo.json", "not-json\n");
17271+
const taskGraphSpy = vi.spyOn(turboModule, "turboTaskGraph");
17272+
const project = await detectProject(root);
17273+
17274+
await expect(mapFeatures(root, project, [])).rejects.toThrow();
17275+
await writeFixture(root, "turbo.json", JSON.stringify({ tasks: {} }));
17276+
await expect(mapFeatures(root, project, [])).resolves.toBeDefined();
1720317277

17204-
expect(spy).toHaveBeenCalledTimes(1);
17278+
expect(taskGraphSpy).toHaveBeenCalledTimes(2);
1720517279
});
1720617280
});

0 commit comments

Comments
 (0)