diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/package.json b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/package.json index 54da4b76..6663a142 100644 --- a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/package.json +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/package.json @@ -1,6 +1,6 @@ { "name": "@query-doctor/sqlcommenter-typeorm", - "version": "0.0.2", + "version": "0.1.0", "description": "SQLCommenter patch for TypeORM", "main": "dist/cjs/index.js", "type": "module", diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/src/index.ts b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/src/index.ts index 0824b9e8..4ed1186c 100644 --- a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/src/index.ts +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/src/index.ts @@ -1,6 +1,7 @@ import { alreadyHasTrailingComment, serializeTags, type Tag } from "./sqlcommenter.js"; import { als } from "./als.js"; import { pushW3CTraceContext } from "./tracing.js"; +import { resolveFilePath } from "./path.js"; const LIBRARY_NAME = "sqlcommenter-typeorm"; @@ -47,7 +48,7 @@ export function traceCaller(): string | undefined { } const match = methodCaller.match(filepathRegex); if (match) { - return match[1]; + return resolveFilePath(match[1]); } } diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/src/path.ts b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/src/path.ts new file mode 100644 index 00000000..811866bd --- /dev/null +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/src/path.ts @@ -0,0 +1,80 @@ +import { existsSync } from "node:fs"; +import { dirname, join } from "node:path"; + +let cachedProjectRoot: string | undefined; + +/** + * Finds the project root by walking up from `process.cwd()` looking for `tsconfig.json`. + * + * In deployed environments, `process.cwd()` may not be the project root + * (e.g., `cd .amplify-hosting/compute/default/ && node app.js`). + * Walking up to find `tsconfig.json` — which is never copied to deployment directories — + * gives us the real project root. + * + * The result is cached since the project root doesn't change during a process's lifetime. + */ +export function findProjectRoot(): string { + if (cachedProjectRoot !== undefined) { + return cachedProjectRoot; + } + let projectRoot = process.cwd(); + for (let d = projectRoot; d !== dirname(d); d = dirname(d)) { + if (existsSync(join(d, "tsconfig.json"))) { + projectRoot = d; + break; + } + } + cachedProjectRoot = projectRoot; + return projectRoot; +} + +/** + * Resolves a file path from a stack trace to a correct absolute path. + * + * When compiled JS is relocated (e.g., postbuild copies `dist/` to a deployment directory), + * source-map-resolved paths become incorrect because the relative `sources` entries in + * `.map` files resolve against the new location instead of the original project. + * + * This extracts the `src/`-relative portion and reconstructs the path using the real + * project root. + * + * @param raw - A stack trace entry like "/wrong/path/src/routes/admin.ts:12:15" + * @returns The resolved path like "/project/root/src/routes/admin.ts:12:15" + */ +export function resolveFilePath(raw: string): string { + // Split off :line:column suffix + const match = raw.match(/^(.*?):(\d+:\d+)$/); + if (!match) { + return raw; + } + const [, filePath, lineCol] = match; + const srcIdx = filePath.indexOf("src/"); + if (srcIdx < 0) { + return raw; + } + const projectRoot = findProjectRoot(); + const relativePath = filePath.substring(srcIdx); + const resolved = `${projectRoot}/${relativePath}`; + return `${applyWslPrefix(resolved)}:${lineCol}`; +} + +/** + * Prefixes an absolute path with the WSL network path when running inside WSL. + * + * Inside WSL, absolute paths like `/home/user/project/...` can't be resolved + * from Windows-side tooling (e.g., clickable links in dashboards or VS Code). + * The `WSL_DISTRO_NAME` env var is always set inside WSL, and the path format + * `//wsl.localhost//...` makes paths accessible from Windows. + */ +export function applyWslPrefix(filePath: string): string { + const distro = process.env.WSL_DISTRO_NAME; + if (distro) { + return `//wsl.localhost/${distro}${filePath}`; + } + return filePath; +} + +/** @internal Exposed for testing only */ +export function _resetProjectRootCache() { + cachedProjectRoot = undefined; +} diff --git a/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/test/path.spec.ts b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/test/path.spec.ts new file mode 100644 index 00000000..a379bb32 --- /dev/null +++ b/nodejs/sqlcommenter-nodejs/packages/sqlcommenter-typeorm/test/path.spec.ts @@ -0,0 +1,121 @@ +import { test } from "node:test"; +import assert from "node:assert"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { + findProjectRoot, + resolveFilePath, + applyWslPrefix, + _resetProjectRootCache, +} from "../src/path.js"; + +test("findProjectRoot", async (t) => { + t.afterEach(() => { + _resetProjectRootCache(); + }); + + await t.test("returns a directory containing tsconfig.json", () => { + const root = findProjectRoot(); + assert.ok( + existsSync(join(root, "tsconfig.json")), + `Expected ${root} to contain tsconfig.json`, + ); + }); + + await t.test("caches the result across calls", () => { + const first = findProjectRoot(); + const second = findProjectRoot(); + assert.strictEqual(first, second); + }); +}); + +test("resolveFilePath", async (t) => { + t.afterEach(() => { + _resetProjectRootCache(); + }); + + await t.test("resolves path with src/ to project root", () => { + const projectRoot = findProjectRoot(); + const result = resolveFilePath( + "/wrong/deploy/dir/src/routes/admin.ts:12:15", + ); + assert.strictEqual(result, `${projectRoot}/src/routes/admin.ts:12:15`); + }); + + await t.test("leaves path without src/ unchanged", () => { + const result = resolveFilePath("/some/other/path/routes/admin.ts:5:10"); + assert.strictEqual(result, "/some/other/path/routes/admin.ts:5:10"); + }); + + await t.test("preserves line:column suffix", () => { + const projectRoot = findProjectRoot(); + const result = resolveFilePath("/bad/path/src/index.ts:99:3"); + assert.strictEqual(result, `${projectRoot}/src/index.ts:99:3`); + }); + + await t.test("uses first src/ occurrence", () => { + const projectRoot = findProjectRoot(); + const result = resolveFilePath( + "/deploy/src/nested/src/routes/admin.ts:1:1", + ); + assert.strictEqual( + result, + `${projectRoot}/src/nested/src/routes/admin.ts:1:1`, + ); + }); + + await t.test("returns raw string if no line:column suffix", () => { + const result = resolveFilePath("/some/path/src/file.ts"); + assert.strictEqual(result, "/some/path/src/file.ts"); + }); +}); + +test("applyWslPrefix", async (t) => { + const originalWslDistro = process.env.WSL_DISTRO_NAME; + + t.afterEach(() => { + if (originalWslDistro === undefined) { + delete process.env.WSL_DISTRO_NAME; + } else { + process.env.WSL_DISTRO_NAME = originalWslDistro; + } + }); + + await t.test("prefixes path when WSL_DISTRO_NAME is set", () => { + process.env.WSL_DISTRO_NAME = "Ubuntu"; + const result = applyWslPrefix("/home/user/project/src/index.ts"); + assert.strictEqual( + result, + "//wsl.localhost/Ubuntu/home/user/project/src/index.ts", + ); + }); + + await t.test("returns path unchanged when WSL_DISTRO_NAME is not set", () => { + delete process.env.WSL_DISTRO_NAME; + const result = applyWslPrefix("/home/user/project/src/index.ts"); + assert.strictEqual(result, "/home/user/project/src/index.ts"); + }); +}); + +test("resolveFilePath with WSL", async (t) => { + const originalWslDistro = process.env.WSL_DISTRO_NAME; + + t.afterEach(() => { + _resetProjectRootCache(); + if (originalWslDistro === undefined) { + delete process.env.WSL_DISTRO_NAME; + } else { + process.env.WSL_DISTRO_NAME = originalWslDistro; + } + }); + + await t.test("applies WSL prefix to resolved src/ paths", () => { + process.env.WSL_DISTRO_NAME = "Ubuntu"; + const projectRoot = findProjectRoot(); + const result = resolveFilePath("/wrong/path/src/routes/admin.ts:12:15"); + assert.strictEqual( + result, + `//wsl.localhost/Ubuntu${projectRoot}/src/routes/admin.ts:12:15`, + ); + }); +});