Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ jobs:
node-version: ${{matrix.node-version}}
cache: yarn
- run: yarn
- uses: actions/cache@v4
with:
path: |
test/pnpm-compat/.yarn/cache
test/pnpm-compat/.yarn/global
key: pnpm-compat-yarn-${{ runner.os }}-${{ hashFiles('test/pnpm-compat/yarn.lock') }}
- run: node test/smoketest.mjs
- run: yarn test
env:
Expand All @@ -57,6 +63,12 @@ jobs:
with:
cache: yarn
- run: yarn
- uses: actions/cache@v4
with:
path: |
test/pnpm-compat/.yarn/cache
test/pnpm-compat/.yarn/global
key: pnpm-compat-yarn-${{ runner.os }}-${{ hashFiles('test/pnpm-compat/yarn.lock') }}
- run: yarn prepack
- run: yarn test || yarn jest --no-silent --verbose --onlyFailures
- run: node test/smoketest.mjs
Expand Down
874 changes: 0 additions & 874 deletions .yarn/releases/yarn-3.6.3.cjs

This file was deleted.

940 changes: 940 additions & 0 deletions .yarn/releases/yarn-4.13.0.cjs

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
nodeLinker: pnpm
compressionLevel: mixed

enableGlobalCache: true

yarnPath: .yarn/releases/yarn-3.6.3.cjs
nodeLinker: node-modules

npmMinimalAgeGate: 2880

yarnPath: .yarn/releases/yarn-4.13.0.cjs
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"type": "git",
"url": "git+https://github.com/getappmap/appmap-node.git"
},
"packageManager": "yarn@3.6.3",
"packageManager": "yarn@4.13.0",
"type": "commonjs",
"engines": {
"node": ">=18"
Expand Down
5 changes: 3 additions & 2 deletions src/hooks/jest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ import {
} from "../recorder";
import genericTranform from "../transform";
import { isId } from "../util/isId";
import { matchesPackageFile } from "../util/matchesPackageFile";

export function shouldInstrument(url: URL): boolean {
return (
url.pathname.endsWith("jest-runtime/build/index.js") ||
url.pathname.endsWith("jest-circus/build/state.js")
matchesPackageFile(url.pathname, "jest-runtime", "build/index.js") ||
matchesPackageFile(url.pathname, "jest-circus", "build/state.js")
);
}

Expand Down
3 changes: 2 additions & 1 deletion src/hooks/mocha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import {
getTestRecording,
startTestRecording,
} from "../recorder";
import { matchesPackageFile } from "../util/matchesPackageFile";

export function shouldInstrument(url: URL): boolean {
return url.pathname.endsWith("/mocha/lib/runner.js");
return matchesPackageFile(url.pathname, "mocha", "lib/runner.js");
}

export function transform(program: ESTree.Program): ESTree.Program {
Expand Down
32 changes: 13 additions & 19 deletions src/hooks/vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
startTestRecording,
} from "../recorder";
import genericTransform from "../transform";
import { matchesPackageFile } from "../util/matchesPackageFile";

function createInitChannel() {
return new worker.BroadcastChannel("appmap-node/vitest/initialized");
Expand Down Expand Up @@ -63,27 +64,18 @@ if (shouldListenForInit()) {
};
}

const vitestRunnerIndexJsFilePathEnding = "/@vitest/runner/dist/index.js";
// vitest v3 splits runTest into chunk-hooks.js; index.js is just re-exports
const vitestRunnerChunkHooksJsFilePathEnding = "/@vitest/runner/dist/chunk-hooks.js";
const viteNodeClientMjsFilePathEnding = "/vite-node/dist/client.mjs";
// vitest v4 uses vite's built-in module runner instead of vite-node
const viteModuleRunnerJsFilePathEnding = "/vite/dist/node/module-runner.js";
// vitest v4 uses VitestModuleEvaluator instead of ESModulesEvaluator
const vitestModuleEvaluatorJsFilePathEnding = "/vitest/dist/module-evaluator.js";

export function shouldInstrument(url: URL): boolean {
// 1. …/vite-node/dist/client.mjs ViteNodeRunner.runModule (vitest v0-v3)
// or …/vite/dist/node/module-runner.js ESModulesEvaluator.runInlinedModule (vitest v4)
// is the place to transform test and user files
// 2. @vitest/runner/dist/index.js (or chunk-hooks.js for v3) runTest
// is the place to intercept test before and afters
return (
url.pathname.endsWith(vitestRunnerIndexJsFilePathEnding) ||
url.pathname.endsWith(vitestRunnerChunkHooksJsFilePathEnding) ||
url.pathname.endsWith(viteNodeClientMjsFilePathEnding) ||
url.pathname.endsWith(viteModuleRunnerJsFilePathEnding) ||
url.pathname.endsWith(vitestModuleEvaluatorJsFilePathEnding)
matchesPackageFile(url.pathname, "@vitest/runner", "dist/index.js") ||
matchesPackageFile(url.pathname, "@vitest/runner", "dist/chunk-hooks.js") ||
matchesPackageFile(url.pathname, "vite-node", "dist/client.mjs") ||
matchesPackageFile(url.pathname, "vite", "dist/node/module-runner.js") ||
matchesPackageFile(url.pathname, "vitest", "dist/module-evaluator.js")
);
}

Expand Down Expand Up @@ -196,15 +188,16 @@ function patchRunInlinedModule(md: ESTree.MethodDefinition) {
export function transform(program: ESTree.Program): ESTree.Program {
const source = program.loc?.source;
if (
source?.endsWith(vitestRunnerIndexJsFilePathEnding) ||
source?.endsWith(vitestRunnerChunkHooksJsFilePathEnding)
source &&
(matchesPackageFile(source, "@vitest/runner", "dist/index.js") ||
matchesPackageFile(source, "@vitest/runner", "dist/chunk-hooks.js"))
)
walk(program, {
FunctionDeclaration(fd: ESTree.FunctionDeclaration) {
if (fd.id?.name === "runTest") patchRunTest(fd);
},
});
else if (source?.endsWith(viteNodeClientMjsFilePathEnding))
else if (source && matchesPackageFile(source, "vite-node", "dist/client.mjs"))
walk(program, {
ClassDeclaration(cd: ESTree.ClassDeclaration) {
if (cd.id?.name === "ViteNodeRunner") {
Expand All @@ -217,8 +210,9 @@ export function transform(program: ESTree.Program): ESTree.Program {
},
});
else if (
source?.endsWith(viteModuleRunnerJsFilePathEnding) ||
source?.endsWith(vitestModuleEvaluatorJsFilePathEnding)
source &&
(matchesPackageFile(source, "vite", "dist/node/module-runner.js") ||
matchesPackageFile(source, "vitest", "dist/module-evaluator.js"))
)
walk(program, {
MethodDefinition(md: ESTree.MethodDefinition) {
Expand Down
157 changes: 157 additions & 0 deletions src/util/__tests__/matchesPackageFile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { matchesPackageFile } from "../matchesPackageFile";

describe("matchesPackageFile", () => {
describe("classic layout (npm, pnpm, yarn classic)", () => {
it("matches a simple package", () => {
expect(
matchesPackageFile("/app/node_modules/mocha/lib/runner.js", "mocha", "lib/runner.js"),
).toBe(true);
});

it("matches a scoped package", () => {
expect(
matchesPackageFile(
"/app/node_modules/@vitest/runner/dist/index.js",
"@vitest/runner",
"dist/index.js",
),
).toBe(true);
});

it("matches inside a nested node_modules", () => {
expect(
matchesPackageFile(
"/app/node_modules/some-dep/node_modules/mocha/lib/runner.js",
"mocha",
"lib/runner.js",
),
).toBe(true);
});

it("does not match a different package with the same file path", () => {
expect(
matchesPackageFile(
"/app/node_modules/jest-runtime/build/index.js",
"jest-circus",
"build/index.js",
),
).toBe(false);
});

it("does not match a package whose name is a suffix of another", () => {
expect(
matchesPackageFile("/app/node_modules/mocha-extra/lib/runner.js", "mocha", "lib/runner.js"),
).toBe(false);
});

it("does not match when the file path differs", () => {
expect(
matchesPackageFile("/app/node_modules/mocha/lib/mocha.js", "mocha", "lib/runner.js"),
).toBe(false);
});

it("does not match when the separator before the package name is missing", () => {
expect(matchesPackageFile("xmocha/lib/runner.js", "mocha", "lib/runner.js")).toBe(false);
});
});

describe("Yarn 4 pnpm linker layout (.store/pkg-npm-ver-hash/package/)", () => {
it("matches a simple package with npm store entry", () => {
expect(
matchesPackageFile(
"/app/node_modules/.store/mocha-npm-10.2.0-abc123def/package/lib/runner.js",
"mocha",
"lib/runner.js",
),
).toBe(true);
});

it("matches a simple package with virtual store entry", () => {
expect(
matchesPackageFile(
"/app/node_modules/.store/mocha-virtual-abc123def/package/lib/runner.js",
"mocha",
"lib/runner.js",
),
).toBe(true);
});

it("matches a scoped package (slash replaced with dash in store name)", () => {
expect(
matchesPackageFile(
"/app/node_modules/.store/@vitest-runner-npm-1.6.0-abc123/package/dist/index.js",
"@vitest/runner",
"dist/index.js",
),
).toBe(true);
});

it("matches with a multi-segment file path", () => {
expect(
matchesPackageFile(
"/app/node_modules/.store/vite-npm-5.0.0-abc123/package/dist/node/module-runner.js",
"vite",
"dist/node/module-runner.js",
),
).toBe(true);
});

it("does not match when store entry is for a different package", () => {
expect(
matchesPackageFile(
"/app/node_modules/.store/jest-runtime-npm-29.0.0-abc123/package/build/index.js",
"jest-circus",
"build/index.js",
),
).toBe(false);
});

it("does not match /package/ path without a .store prefix", () => {
expect(
matchesPackageFile("/app/some-dir/package/lib/runner.js", "mocha", "lib/runner.js"),
).toBe(false);
});

it("does not match when package name is a prefix of the store entry name", () => {
expect(
matchesPackageFile(
"/app/node_modules/.store/mocha-extra-npm-1.0.0-abc123/package/lib/runner.js",
"mocha",
"lib/runner.js",
),
).toBe(false);
});
});

describe("Yarn 3 pnpm linker layout (.store/pkg-npm-ver-hash/pkg/)", () => {
it("matches a simple package", () => {
expect(
matchesPackageFile(
"/app/node_modules/.store/mocha-npm-10.2.0-abc123/mocha/lib/runner.js",
"mocha",
"lib/runner.js",
),
).toBe(true);
});

it("matches a scoped package", () => {
expect(
matchesPackageFile(
"/app/node_modules/.store/@vitest-runner-npm-1.6.0-abc123/@vitest/runner/dist/index.js",
"@vitest/runner",
"dist/index.js",
),
).toBe(true);
});
});

describe("edge cases", () => {
it("returns false for an empty string", () => {
expect(matchesPackageFile("", "mocha", "lib/runner.js")).toBe(false);
});

it("matches when the path starts directly with the package/file suffix", () => {
expect(matchesPackageFile("/mocha/lib/runner.js", "mocha", "lib/runner.js")).toBe(true);
});
});
});
27 changes: 27 additions & 0 deletions src/util/matchesPackageFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Checks if a URL or path string matches a specific package file, handling multiple
* node_modules layouts:
* - Classic (npm, pnpm, yarn classic): .../node_modules/{pkg}/{file}
* - Yarn 3 pnpm linker: .../node_modules/.store/{pkg}-npm-{ver}-{hash}/{pkg}/{file}
* - Yarn 4 pnpm linker: .../node_modules/.store/{pkg}-npm-{ver}-{hash}/package/{file}
*/
export function matchesPackageFile(
urlOrPath: string,
packageName: string,
filePath: string,
): boolean {
if (urlOrPath.endsWith(`/${packageName}/${filePath}`)) return true;
// Yarn 4 pnpm linker uses 'package' as the subdirectory name inside .store entries
if (urlOrPath.endsWith(`/package/${filePath}`)) {
// Yarn 4 pnpm linker names store entries as:
// @scope/pkg → @scope-pkg-npm-ver-hash or @scope-pkg-virtual-hash
// pkg → pkg-npm-ver-hash or pkg-virtual-hash
const sanitized = packageName.replace("/", "-");
if (
urlOrPath.includes(`/.store/${sanitized}-npm-`) ||
urlOrPath.includes(`/.store/${sanitized}-virtual-`)
)
return true;
}
return false;
}
2 changes: 1 addition & 1 deletion test/__snapshots__/next16.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ exports[`mapping a Next.js 16 appmap 1`] = `
"http_server_response": {
"headers": {
"Cache-Control": "no-store, must-revalidate",
"Content-Length": "2238",
"Content-Length": "2210",
"Content-Type": "text/html; charset=utf-8",
"Vary": "Accept-Encoding",
"X-Powered-By": "Next.js",
Expand Down
Loading
Loading