123 lines
4.9 KiB
TypeScript
123 lines
4.9 KiB
TypeScript
// Exact-version peer-dependency gate, shared by every optional-SDK bootstrap
|
|
// (OpenTelemetry in `instrumentation.ts`, Sentry in `sentry.ts`).
|
|
//
|
|
// This module has no module-init side effect — it only defines functions. A
|
|
// bootstrap module imports it and calls `checkExactPeerVersions` itself. That
|
|
// matters for `sentry.ts`: a direct import of `instrumentation.ts` would run
|
|
// the OpenTelemetry bootstrap (`instrumentationReady`) as a side effect of
|
|
// loading the Sentry gate, which this module avoids.
|
|
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import { dirname, join } from "node:path";
|
|
|
|
/**
|
|
* Read this package's own `peerDependencies`, so the exact-version gate
|
|
* compares an installed package against the same version this manifest
|
|
* declares — one source of truth, not a second hardcoded copy. Returns an
|
|
* empty map on any read or parse failure (fail open: an unreadable manifest
|
|
* skips the version check rather than blocking startup).
|
|
*/
|
|
function readOwnPeerDependencies(): Record<string, string> {
|
|
try {
|
|
const pkgUrl = new URL("../package.json", import.meta.url);
|
|
const raw = readFileSync(pkgUrl, "utf8");
|
|
const parsed = JSON.parse(raw) as { peerDependencies?: Record<string, string> };
|
|
return parsed.peerDependencies ?? {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read an installed package's own declared `version`, without importing or
|
|
* executing the package. Resolves the package's main entry point (which
|
|
* respects its `exports` map) and then walks up the filesystem to the
|
|
* nearest `package.json` whose `name` matches — a direct
|
|
* `require.resolve(\`${packageName}/package.json\`)` throws for a package
|
|
* whose `exports` map does not expose `./package.json` as a subpath, which
|
|
* several `@opentelemetry/*` packages do not, even though the package is
|
|
* correctly installed. Returns null when the package cannot be resolved or no
|
|
* matching `package.json` is found.
|
|
*/
|
|
function readInstalledPackageVersion(packageName: string): string | null {
|
|
try {
|
|
const require = createRequire(import.meta.url);
|
|
let dir = dirname(require.resolve(packageName));
|
|
for (;;) {
|
|
const candidate = join(dir, "package.json");
|
|
if (existsSync(candidate)) {
|
|
const parsed = JSON.parse(readFileSync(candidate, "utf8")) as {
|
|
name?: unknown;
|
|
version?: unknown;
|
|
};
|
|
if (parsed.name === packageName) {
|
|
return typeof parsed.version === "string" ? parsed.version : null;
|
|
}
|
|
}
|
|
const parent = dirname(dir);
|
|
if (parent === dir) return null;
|
|
dir = parent;
|
|
}
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify that every package in `packageNames` is installed at the exact
|
|
* version `peerDependencies` declares. The caller passes only the packages it
|
|
* needs checked — the OpenTelemetry bootstrap passes the four common packages
|
|
* plus the one exporter `OTEL_EXPORTER_OTLP_PROTOCOL` selected, and the
|
|
* Sentry gate passes `["@sentry/node"]`. Never throws: a missing manifest, a
|
|
* missing package, or an unreadable `package.json` all resolve to a reported
|
|
* issue, not an exception.
|
|
*
|
|
* `peerDependencies` defaults to this manifest's own declared versions
|
|
* (`readOwnPeerDependencies()`), which is what each bootstrap uses. A test
|
|
* passes an explicit map instead, so it can check the comparison logic
|
|
* against a package it controls without writing into `node_modules`.
|
|
*
|
|
* The returned `diagnostic` string names the OpenTelemetry endpoint variable,
|
|
* because the OpenTelemetry bootstrap logs it directly. The Sentry gate reads
|
|
* `detail` instead and builds its own diagnostic line — see `sentry.ts`.
|
|
*/
|
|
export function checkExactPeerVersions(
|
|
packageNames: readonly string[],
|
|
peerDependencies: Record<string, string> = readOwnPeerDependencies(),
|
|
): { ok: true } | { ok: false; diagnostic: string; detail: unknown } {
|
|
const missing: string[] = [];
|
|
const mismatched: { name: string; installed: string; expected: string }[] = [];
|
|
|
|
for (const name of packageNames) {
|
|
const expected = peerDependencies[name];
|
|
const installed = readInstalledPackageVersion(name);
|
|
if (installed === null) {
|
|
missing.push(name);
|
|
} else if (expected && installed !== expected) {
|
|
mismatched.push({ name, installed, expected });
|
|
}
|
|
}
|
|
|
|
if (missing.length === 0 && mismatched.length === 0) return { ok: true };
|
|
|
|
const parts: string[] = [];
|
|
if (missing.length > 0) {
|
|
parts.push(`the @opentelemetry/* packages are not installed: ${missing.join(", ")}`);
|
|
}
|
|
if (mismatched.length > 0) {
|
|
const detail = mismatched
|
|
.map((m) => `${m.name}@${m.installed} (expected ${m.expected})`)
|
|
.join(", ");
|
|
parts.push(`a package is installed at an unsupported version: ${detail}`);
|
|
}
|
|
|
|
return {
|
|
ok: false,
|
|
diagnostic:
|
|
`[paperclip] OTEL_EXPORTER_OTLP_ENDPOINT is set but ${parts.join("; and ")}. ` +
|
|
"Continuing without tracing.",
|
|
detail: { missing, mismatched },
|
|
};
|
|
}
|