Merge pull request #3957 from jeremymcs/fix/mcp-importLocalModule-paths
fix(mcp-server): importLocalModule resolves src/ paths from dist/ context
This commit is contained in:
commit
9a7453fae8
2 changed files with 82 additions and 1 deletions
48
packages/mcp-server/src/import-candidates.test.ts
Normal file
48
packages/mcp-server/src/import-candidates.test.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// GSD-2 — Regression tests for importLocalModule candidate resolution (#3954)
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { _buildImportCandidates } from "./workflow-tools.js";
|
||||
|
||||
describe("_buildImportCandidates", () => {
|
||||
it("includes dist/ fallback for src/ paths", () => {
|
||||
const candidates = _buildImportCandidates("../../../src/resources/extensions/gsd/db-writer.js");
|
||||
assert.ok(
|
||||
candidates.some((c) => c.includes("/dist/resources/extensions/gsd/db-writer.js")),
|
||||
"should include dist/ swapped candidate",
|
||||
);
|
||||
});
|
||||
|
||||
it("includes src/ fallback for dist/ paths", () => {
|
||||
const candidates = _buildImportCandidates("../../../dist/resources/extensions/gsd/db-writer.js");
|
||||
assert.ok(
|
||||
candidates.some((c) => c.includes("/src/resources/extensions/gsd/db-writer.js")),
|
||||
"should include src/ swapped candidate",
|
||||
);
|
||||
});
|
||||
|
||||
it("includes .ts variants for .js paths", () => {
|
||||
const candidates = _buildImportCandidates("../../../src/resources/extensions/gsd/db-writer.js");
|
||||
assert.ok(
|
||||
candidates.some((c) => c.endsWith("db-writer.ts") && c.includes("/src/")),
|
||||
"should include .ts variant for original src/ path",
|
||||
);
|
||||
assert.ok(
|
||||
candidates.some((c) => c.endsWith("db-writer.ts") && c.includes("/dist/")),
|
||||
"should include .ts variant for swapped dist/ path",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns original path first", () => {
|
||||
const input = "../../../src/resources/extensions/gsd/db-writer.js";
|
||||
const candidates = _buildImportCandidates(input);
|
||||
assert.equal(candidates[0], input, "first candidate should be the original path");
|
||||
});
|
||||
|
||||
it("handles paths without src/ or dist/ gracefully", () => {
|
||||
const candidates = _buildImportCandidates("./local-module.js");
|
||||
assert.equal(candidates.length, 2, "should have original + .ts variant only");
|
||||
assert.equal(candidates[0], "./local-module.js");
|
||||
assert.equal(candidates[1], "./local-module.ts");
|
||||
});
|
||||
});
|
||||
|
|
@ -326,6 +326,7 @@ function getWriteGateModuleCandidates(): string[] {
|
|||
|
||||
candidates.push(
|
||||
new URL("../../../src/resources/extensions/gsd/bootstrap/write-gate.js", import.meta.url).href,
|
||||
new URL("../../../dist/resources/extensions/gsd/bootstrap/write-gate.js", import.meta.url).href,
|
||||
new URL("../../../src/resources/extensions/gsd/bootstrap/write-gate.ts", import.meta.url).href,
|
||||
);
|
||||
|
||||
|
|
@ -336,8 +337,39 @@ function toFileUrl(modulePath: string): string {
|
|||
return pathToFileURL(resolve(modulePath)).href;
|
||||
}
|
||||
|
||||
/** @internal — exported for testing only */
|
||||
export function _buildImportCandidates(relativePath: string): string[] {
|
||||
// Build candidate paths: try the given path first, then swap src/<->dist/
|
||||
// and try .ts extension. This handles both dev (tsx from src/) and prod
|
||||
// (compiled from dist/) execution contexts.
|
||||
const candidates: string[] = [relativePath];
|
||||
const swapped = relativePath.includes("/src/")
|
||||
? relativePath.replace("/src/", "/dist/")
|
||||
: relativePath.includes("/dist/")
|
||||
? relativePath.replace("/dist/", "/src/")
|
||||
: null;
|
||||
if (swapped) candidates.push(swapped);
|
||||
// Also try .ts variants for dev-mode tsx execution
|
||||
if (relativePath.endsWith(".js")) {
|
||||
candidates.push(relativePath.replace(/\.js$/, ".ts"));
|
||||
if (swapped) candidates.push(swapped.replace(/\.js$/, ".ts"));
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
async function importLocalModule<T>(relativePath: string): Promise<T> {
|
||||
return import(new URL(relativePath, import.meta.url).href) as Promise<T>;
|
||||
const candidates = _buildImportCandidates(relativePath)
|
||||
.map((p) => new URL(p, import.meta.url).href);
|
||||
|
||||
let lastErr: unknown;
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
return await import(candidate) as T;
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
}
|
||||
}
|
||||
throw lastErr;
|
||||
}
|
||||
|
||||
function getWorkflowExecutorModuleCandidates(env: NodeJS.ProcessEnv = process.env): string[] {
|
||||
|
|
@ -352,6 +384,7 @@ function getWorkflowExecutorModuleCandidates(env: NodeJS.ProcessEnv = process.en
|
|||
|
||||
candidates.push(
|
||||
new URL("../../../src/resources/extensions/gsd/tools/workflow-tool-executors.js", import.meta.url).href,
|
||||
new URL("../../../dist/resources/extensions/gsd/tools/workflow-tool-executors.js", import.meta.url).href,
|
||||
new URL("../../../src/resources/extensions/gsd/tools/workflow-tool-executors.ts", import.meta.url).href,
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue