diff --git a/src/Recording.ts b/src/Recording.ts index cd835e3e..86e62855 100644 --- a/src/Recording.ts +++ b/src/Recording.ts @@ -6,7 +6,7 @@ import { inspect } from "node:util"; import type AppMap from "./AppMap"; import AppMapStream from "./AppMapStream"; import { makeClassMap } from "./classMap"; -import { appMapDir } from "./config"; +import config from "./config"; import { makeCallEvent, makeExceptionEvent, makeReturnEvent } from "./event"; import { defaultMetadata } from "./metadata"; import type { FunctionInfo } from "./registry"; @@ -16,7 +16,7 @@ export default class Recording { constructor(type: AppMap.RecorderType, recorder: string, ...names: string[]) { const dirs = [recorder, ...names].map(quotePathSegment); const name = dirs.pop()!; // it must have at least one element - this.path = join(appMapDir, ...dirs, makeAppMapFilename(name)); + this.path = join(config.appmapDir, ...dirs, makeAppMapFilename(name)); this.partPath = this.path + ".part"; this.stream = new AppMapStream(this.partPath); this.metadata = { diff --git a/src/bin.ts b/src/bin.ts index 84761107..1fe4eda8 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -6,8 +6,9 @@ import { kill, pid } from "node:process"; import { info } from "./message"; import { version } from "./metadata"; -import { readPkgUp } from "./util/readPkgUp"; import forwardSignals from "./util/forwardSignals"; +import { readPkgUp } from "./util/readPkgUp"; +import config from "./config"; const registerPath = resolve(__dirname, "../dist/register.js"); const loaderPath = resolve(__dirname, "../dist/loader.js"); @@ -19,6 +20,7 @@ export function main() { info("Running with appmap-node version %s", version); addNodeOptions("--require", registerPath); + config.export(); // FIXME: Probably there should be a way to remove this altogether // by changing our custom loader implementation diff --git a/src/config.ts b/src/config.ts index 2d6156b0..90f30ee1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,8 +3,19 @@ import { cwd } from "node:process"; import { readPkgUp } from "./util/readPkgUp"; -export const root = cwd(); -export const appMapDir = join(root, "tmp", "appmap"); +export class Config { + public readonly appmapDir: string; + public readonly appName: string; -export const targetPackage = readPkgUp(root); -export const appName = targetPackage?.name ?? dirname(root); + constructor(public readonly root = process.env.APPMAP_ROOT ?? cwd()) { + this.appmapDir = join(root, "tmp", "appmap"); + const targetPackage = readPkgUp(root); + this.appName = targetPackage?.name ?? dirname(root); + } + + export() { + process.env.APPMAP_ROOT = this.root; + } +} + +export default new Config(); diff --git a/src/hooks/__tests__/instrument.test.ts b/src/hooks/__tests__/instrument.test.ts index 884348da..b7bf60b6 100644 --- a/src/hooks/__tests__/instrument.test.ts +++ b/src/hooks/__tests__/instrument.test.ts @@ -1,11 +1,12 @@ import { full as walk } from "acorn-walk"; import { ESTree, parse } from "meriyah"; -import * as instrument from "../instrument"; +import config from "../../config"; import * as registry from "../../registry"; +import * as instrument from "../instrument"; describe(instrument.shouldInstrument, () => { - instrument.setRoot("/test"); + jest.replaceProperty(config, "root", "/test"); test.each([ ["node:test", false], ["file:///test/test.json", false], @@ -125,3 +126,5 @@ function stripLocations(program: ESTree.Program): ESTree.Program { }); return program; } + +jest.mock("../../config"); diff --git a/src/hooks/instrument.ts b/src/hooks/instrument.ts index f134efbe..82522be4 100644 --- a/src/hooks/instrument.ts +++ b/src/hooks/instrument.ts @@ -1,12 +1,12 @@ import assert from "node:assert"; import path from "node:path"; -import { cwd } from "node:process"; import { fileURLToPath } from "node:url"; import { ancestor as walk } from "acorn-walk"; import { ESTree, parse } from "meriyah"; import { SourceMapConsumer } from "source-map-js"; +import config from "../config"; import { args as args_, call_, @@ -19,7 +19,7 @@ import { toArrowFunction, yieldStar, } from "../generate"; -import { FunctionInfo, SourceLocation, createMethodInfo, createFunctionInfo } from "../registry"; +import { FunctionInfo, SourceLocation, createFunctionInfo, createMethodInfo } from "../registry"; import findLast from "../util/findLast"; const __appmapFunctionRegistryIdentifier = identifier("__appmapFunctionRegistry"); @@ -147,19 +147,13 @@ function wrapWithRecord( return wrapped; } -let root = cwd(); - -export function setRoot(path: string) { - root = path; -} - export function shouldInstrument(url: URL): boolean { if (url.protocol !== "file:") return false; if (url.pathname.endsWith(".json")) return false; const filePath = fileURLToPath(url); if (filePath.includes("node_modules") || filePath.includes(".yarn")) return false; - if (isUnrelated(root, filePath)) return false; + if (isUnrelated(config.root, filePath)) return false; return true; } diff --git a/src/loader.ts b/src/loader.ts index f53b67ba..001b6a1a 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -1,11 +1,11 @@ -import path from "node:path"; -import { URL } from "node:url"; +import path, { dirname } from "node:path"; +import { URL, fileURLToPath } from "node:url"; import type { NodeLoaderHooksAPI2 } from "ts-node"; -import { targetPackage } from "./config"; import { warn } from "./message"; import transform, { findHook } from "./transform"; +import { readPkgUp } from "./util/readPkgUp"; export const load: NodeLoaderHooksAPI2["load"] = async function load(url, context, defaultLoad) { const urlObj = new URL(url); @@ -15,9 +15,13 @@ export const load: NodeLoaderHooksAPI2["load"] = async function load(url, contex // CommonJS modules. This leads to TypeError [ERR_UNKNOWN_FILE_EXTENSION]. // To fix this, we give the context.format hint to defaultLoad if we decide // that the file is a CommonJS module. - const ext = path.extname(urlObj.pathname).slice(1); - if (ext == "" && urlObj.protocol == "file:" && targetPackage?.type != "module") { - context.format = "commonjs"; + if (urlObj.protocol === "file:") { + const targetPath = fileURLToPath(urlObj); + if ( + path.extname(targetPath).slice(1) === "" && + readPkgUp(dirname(targetPath))?.type !== "module" + ) + context.format = "commonjs"; } const hook = findHook(urlObj); diff --git a/src/metadata.ts b/src/metadata.ts index 4fa59b9c..4da74ee4 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -3,7 +3,7 @@ import process from "node:process"; import type { PackageJson } from "type-fest"; import type AppMap from "./AppMap"; -import { appName } from "./config"; +import config from "./config"; import { examineException } from "./event"; import pick from "./util/pick"; @@ -24,7 +24,7 @@ export const defaultMetadata: Partial & { client: AppMap.Client engine: "Node.js", version: process.version, }, - app: appName, + app: config.appName, }; export function exceptionMetadata(error: unknown): AppMap.ExceptionMetadata | undefined { diff --git a/test/simple.test.ts b/test/simple.test.ts index 68390560..76a1f3dc 100644 --- a/test/simple.test.ts +++ b/test/simple.test.ts @@ -63,3 +63,10 @@ integrationTest("mapping an extensionless CommonJS file", () => { expect(runAppmapNode("./extensionless").status).toBe(0); expect(readAppmap()).toMatchSnapshot(); }); + +integrationTest("running a script after changing the current directory", () => { + // Need to make sure the appmap "root" stays the same after + // appmap-node is run, even if the current directory changes. + expect(runAppmapNode("bash", "-c", "cd subproject; node index.js").status).toBe(0); + expect(readAppmap()).toBeDefined(); +}); diff --git a/test/simple/subproject/index.js b/test/simple/subproject/index.js new file mode 120000 index 00000000..e234193f --- /dev/null +++ b/test/simple/subproject/index.js @@ -0,0 +1 @@ +../index.js \ No newline at end of file