Skip to content
Merged
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
36 changes: 15 additions & 21 deletions .lore.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ async function build(): Promise<void> {
await uploadSourcemapToSentry();

// Clean up intermediate build directory (only the binaries are artifacts).
await rm(BUILD_DIR, { recursive: true, force: true });
// await rm(BUILD_DIR, { recursive: true, force: true });
Comment thread
BYK marked this conversation as resolved.

// Summary
console.log(`\n${"=".repeat(40)}`);
Expand Down
37 changes: 31 additions & 6 deletions script/require-shim.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/**
* ESM preload shim that provides `require` in ESM modules.
* ESM preload shim that provides `require` in ESM modules and handles
* `with { type: "file" }` import attributes in tsx dev mode.
*
* The source code uses bare `require()` for lazy loading (circular dependency
* breaking, optional features). This works natively in Bun and in the CJS
Expand All @@ -8,19 +9,43 @@
*
* The `require` function is anchored at the project root (package.json) so
* that `node:*` builtins and npm package requires resolve correctly. Note
* that relative `require("./foo.js")` calls will resolve from the project
* root, not from the calling file — this is acceptable because all relative
* `require()` calls in `src/` are behind runtime-only code paths (DB init,
* telemetry) that don't execute during tsx script runs.
* that relative `require("./foo.js")` calls resolve from the project root,
* not from the calling file. Files in `src/` that use lazy relative requires
* must use a file-local `createRequire(import.meta.url)` instead of relying
* on this global shim.
*
* `with { type: "file" }` import attributes are used to embed sidecar files
* (e.g. the Ink UI app). Bun supports this natively; esbuild's
* text-import-plugin handles it at build time. In tsx dev mode neither
* applies, so we register a loader hook that returns the file path as a
* string — matching Bun's native behaviour.
*
* Usage: NODE_OPTIONS="--import ./script/require-shim.mjs" tsx script/...
* Or in package.json scripts via the `pnpm tsx` alias.
*/

import { createRequire } from "node:module";
import { createRequire, registerHooks } from "node:module";

if (typeof globalThis.require === "undefined") {
globalThis.require = createRequire(
new URL("../package.json", import.meta.url)
);
}

// Handle `with { type: "file" }` import attributes in Node.js dev mode.
// Bun supports this natively; esbuild's text-import-plugin handles it at
// build time. In tsx dev mode neither applies, so we register a synchronous
// hook that returns the file path as a string — matching Bun's behaviour.
// registerHooks() is available from Node 22.15+ (our minimum).
registerHooks({
load(url, context, nextLoad) {
if (context.importAttributes?.type === "file") {
return {
format: "module",
shortCircuit: true,
source: `export default ${JSON.stringify(new URL(url).pathname)};`,
};
}
return nextLoad(url, context);
},
});
5 changes: 4 additions & 1 deletion src/lib/custom-ca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
*/

import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { rootCertificates } from "node:tls";

const _require = createRequire(import.meta.url);
import { getDefaultCaCert } from "./db/defaults.js";
import { getEnv } from "./env.js";
import { logger } from "./logger.js";
Expand All @@ -30,7 +33,7 @@ import { isSentrySaasUrl } from "./sentry-urls.js";
* option instead.
*/
const setDefaultCACertificates: ((certs: string[]) => void) | undefined = (
require("node:tls") as {
_require("node:tls") as {
setDefaultCACertificates?: (certs: string[]) => void;
}
).setDefaultCACertificates;
Expand Down
7 changes: 6 additions & 1 deletion src/lib/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
*/

import { chmodSync, mkdirSync } from "node:fs";
import { createRequire } from "node:module";
import { join } from "node:path";
import { getEnv } from "../env.js";

const _require = createRequire(import.meta.url);

import { migrateFromJson } from "./migration.js";
import { initSchema, runMigrations } from "./schema.js";
import { Database } from "./sqlite.js";
Expand All @@ -30,7 +34,7 @@ let rawDb: Database | null = null;
let dbOpenedPath: string | null = null;

export function getConfigDir(): string {
const { homedir } = require("node:os");
const { homedir } = _require("node:os");
return (
getEnv()[CONFIG_DIR_ENV_VAR] || join(homedir(), DEFAULT_CONFIG_DIR_NAME)
);
Expand Down Expand Up @@ -107,6 +111,7 @@ export function getDatabase(): Database {
if (getEnv().SENTRY_CLI_NO_TELEMETRY === "1") {
db = rawDb;
} else {
// bare require so esbuild resolves this at bundle time (breaks circular dep)
const { createTracedDatabase } = require("../telemetry.js") as {
createTracedDatabase: (d: Database) => Database;
};
Comment thread
BYK marked this conversation as resolved.
Expand Down
8 changes: 6 additions & 2 deletions src/lib/db/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
*/

import { rmSync } from "node:fs";
import { createRequire } from "node:module";
import { join } from "node:path";

const _require = createRequire(import.meta.url);

import { logger } from "../logger.js";
import { getConfigDir } from "./index.js";
import type { Database } from "./sqlite.js";
Expand Down Expand Up @@ -34,14 +38,14 @@ function markMigrationCompleted(db: Database): void {

function oldConfigExists(): boolean {
const configPath = join(getConfigDir(), OLD_CONFIG_FILENAME);
const { existsSync } = require("node:fs");
const { existsSync } = _require("node:fs");
return existsSync(configPath);
}

function readOldConfig(): OldConfig | null {
const configPath = join(getConfigDir(), OLD_CONFIG_FILENAME);
try {
const { readFileSync } = require("node:fs");
const { readFileSync } = _require("node:fs");
const content = readFileSync(configPath, "utf-8");
return JSON.parse(content);
} catch {
Expand Down
4 changes: 4 additions & 0 deletions src/lib/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@
* - Migration checks
*/

import { createRequire } from "node:module";
import { getEnv } from "../env.js";
import { stringifyUnknown } from "../errors.js";
import { logger } from "../logger.js";
import type { Database } from "./sqlite.js";

const _require = createRequire(import.meta.url);

Check warning on line 20 in src/lib/db/schema.ts

View check run for this annotation

@sentry/warden / warden: find-bugs

[QHA-8WF] Bare `require("../telemetry.js")` in db/index.ts still crashes tsx dev mode when telemetry is enabled (additional location)

This PR moves several lazy relative `require()` calls in `src/` to a file-local `_require = createRequire(import.meta.url)` to fix the dev-mode error `Cannot find module '../telemetry.js'`. However, `src/lib/db/index.ts:115` still uses the bare global `require("../telemetry.js")` (commented as `bare require so esbuild resolves this at bundle time (breaks circular dep)`). In tsx dev mode (`pnpm cli` → `tsx --import ./script/require-shim.mjs src/bin.ts`), the shim's global `require` is anchored at `package.json` in the project root, so `"../telemetry.js"` resolves to `<project-root>/../telemetry.js` and throws `Cannot find module`. Unlike the symmetric bare-require in `telemetry.ts:resolveDbPath()` (lines 1022–1031), which wraps the call in `try/catch` and falls back to a default path, `getDatabase()` lets the error propagate through its outer `try/catch` (which only closes `rawDb` and re-throws). The result: any CLI command that touches the database crashes in dev mode unless `SENTRY_CLI_NO_TELEMETRY=1` is set — the exact failure mode the PR description aims to fix. Fix: either also convert this site to `_require("../telemetry.js")` (keeping a `// @esbuild-keep` style hint or relying on esbuild's bundler still rewriting the file-local require), or wrap it in `try { ... } catch { db = rawDb; }` to degrade gracefully like `resolveDbPath()`.
Comment thread
BYK marked this conversation as resolved.

export const CURRENT_SCHEMA_VERSION = 16;

/** Environment variable to disable auto-repair */
Expand Down Expand Up @@ -671,6 +674,7 @@
let repairSucceeded = false;
try {
// Dynamic imports to avoid circular dependencies with db/index.js
// bare require so esbuild resolves this at bundle time (breaks circular dep)
const { getRawDatabase } = require("./index.js") as {
getRawDatabase: () => Database;
};
Expand Down
5 changes: 4 additions & 1 deletion src/lib/db/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
* Uses `node:sqlite` (Node 22.15+ with `--experimental-sqlite` flag).
*/

import { createRequire } from "node:module";
import { logger } from "../logger.js";

const _require = createRequire(import.meta.url);

const log = logger.withTag("sqlite");

/** Valid SQLite binding value. */
Expand Down Expand Up @@ -58,7 +61,7 @@ function wrapStatement(stmt: any): StatementWrapper {
* Uses `node:sqlite` (Node 22.15+ with `--experimental-sqlite`).
*/
// biome-ignore lint/suspicious/noExplicitAny: driver types loaded lazily
const SqliteImpl: any = require("node:sqlite").DatabaseSync;
const SqliteImpl: any = _require("node:sqlite").DatabaseSync;

/**
* SQLite database wrapper.
Expand Down
8 changes: 6 additions & 2 deletions src/lib/init/ui/ink-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@
*/

import { openSync } from "node:fs";
import { createRequire } from "node:module";
import { ReadStream } from "node:tty";

const _require = createRequire(import.meta.url);

import { setTag } from "@sentry/node-core/light";
import { CLI_VERSION } from "../../constants.js";
import { stripAnsi } from "../../formatters/plain-detect.js";
Expand Down Expand Up @@ -235,7 +239,7 @@ export async function createInkUI(
let isSea = false;
try {
// biome-ignore lint/suspicious/noExplicitAny: node:sea types not yet in @types/node
const sea = require("node:sea") as any;
const sea = _require("node:sea") as any;
isSea = sea.isSea?.() === true;
} catch {
// node:sea not available (older Node or non-SEA context)
Expand All @@ -245,7 +249,7 @@ export async function createInkUI(
// Extract the embedded sidecar to a temp file and import it.
// The asset key matches what fossilize registered via --assets.
// biome-ignore lint/suspicious/noExplicitAny: node:sea types not yet in @types/node
const sea = require("node:sea") as any;
const sea = _require("node:sea") as any;
const { writeFileSync, mkdtempSync } = await import("node:fs");
const { join } = await import("node:path");
const { tmpdir } = await import("node:os");
Expand Down
5 changes: 5 additions & 0 deletions src/lib/list-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@
* buildOrgListCommand
*/

import { createRequire } from "node:module";
import type { Aliases, Command, CommandContext } from "@stricli/core";
import type { SentryContext } from "../context.js";

const _require = createRequire(import.meta.url);

import { parseOrgProjectArg } from "./arg-parsing.js";
import { buildCommand, numberParser } from "./command.js";
import { disableOrgCache } from "./db/regions.js";
Expand Down Expand Up @@ -414,6 +418,7 @@ function getSubcommandsForRoute(routeName: string): Set<string> {
_subcommandsByRoute = new Map();

try {
// bare require so esbuild resolves this at bundle time (breaks circular dep)
const { routes } = require("../app.js") as {
routes: { getAllEntries: () => readonly RouteEntry[] };
};
Expand Down
6 changes: 5 additions & 1 deletion src/lib/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,12 @@
* @module
*/

import { createRequire } from "node:module";
import type { ConsolaInstance } from "consola";
import { createConsola } from "consola";

const _require = createRequire(import.meta.url);

import { getEnv } from "./env.js";

/**
Expand Down Expand Up @@ -209,7 +213,7 @@ export function attachSentryReporter(): void {
// Dynamic import to avoid pulling in Sentry at module load time.
// The reporter is exported from @sentry/node-core/light (via @sentry/node → @sentry/core).
// eslint-disable-next-line @typescript-eslint/no-require-imports
const Sentry = require("@sentry/node-core/light") as {
const Sentry = _require("@sentry/node-core/light") as {
createConsolaReporter: (options?: Record<string, unknown>) => {
log: (logObj: unknown) => void;
};
Expand Down
9 changes: 7 additions & 2 deletions src/lib/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@
*/

import { chmodSync, statSync } from "node:fs";
import { createRequire } from "node:module";
// biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import
import * as Sentry from "@sentry/node-core/light";

const _require = createRequire(import.meta.url);

import { isMusl } from "./binary.js";
import {
CLI_VERSION,
Expand Down Expand Up @@ -383,7 +387,7 @@
const hasGetSystemErrorMap = (() => {
try {
// Dynamic require to avoid bundler issues — the check only matters at runtime
const util = require("node:util") as Record<string, unknown>;
const util = _require("node:util") as Record<string, unknown>;
return typeof util.getSystemErrorMap === "function";
} catch {
return false;
Expand Down Expand Up @@ -1017,9 +1021,10 @@
/** Resolves the database path, falling back to a default if the import fails. */
function resolveDbPath(): string {
try {
// bare require so esbuild resolves this at bundle time (breaks circular dep)
const { getDbPath } = require("./db/index.js") as {
getDbPath: () => string;
};

Check warning on line 1027 in src/lib/telemetry.ts

View check run for this annotation

@sentry/warden / warden: find-bugs

Bare `require("../telemetry.js")` in db/index.ts still crashes tsx dev mode when telemetry is enabled

This PR moves several lazy relative `require()` calls in `src/` to a file-local `_require = createRequire(import.meta.url)` to fix the dev-mode error `Cannot find module '../telemetry.js'`. However, `src/lib/db/index.ts:115` still uses the bare global `require("../telemetry.js")` (commented as `bare require so esbuild resolves this at bundle time (breaks circular dep)`). In tsx dev mode (`pnpm cli` → `tsx --import ./script/require-shim.mjs src/bin.ts`), the shim's global `require` is anchored at `package.json` in the project root, so `"../telemetry.js"` resolves to `<project-root>/../telemetry.js` and throws `Cannot find module`. Unlike the symmetric bare-require in `telemetry.ts:resolveDbPath()` (lines 1022–1031), which wraps the call in `try/catch` and falls back to a default path, `getDatabase()` lets the error propagate through its outer `try/catch` (which only closes `rawDb` and re-throws). The result: any CLI command that touches the database crashes in dev mode unless `SENTRY_CLI_NO_TELEMETRY=1` is set — the exact failure mode the PR description aims to fix. Fix: either also convert this site to `_require("../telemetry.js")` (keeping a `// @esbuild-keep` style hint or relying on esbuild's bundler still rewriting the file-local require), or wrap it in `try { ... } catch { db = rawDb; }` to degrade gracefully like `resolveDbPath()`.
return getDbPath();
} catch {
return "~/.sentry/cli.db";
Expand Down Expand Up @@ -1102,7 +1107,7 @@
repairAttempted = true;

const dbPath = resolveDbPath();
const { dirname } = require("node:path") as {
const { dirname } = _require("node:path") as {
dirname: (p: string) => string;
};
const configDir = dirname(dbPath);
Expand Down
2 changes: 1 addition & 1 deletion test/commands/cli/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -864,7 +864,7 @@ describe("sentry cli setup", () => {
});

test("setup completes gracefully when completion directory is not writable", async () => {
// Make the completions dir unwritable so Bun.write() can't write the
// Make the completions dir unwritable so write() can't write the
// completion script. installCompletions() catches the permission error
// and returns null — setup completes without error or warning.
const { chmodSync: chmod } = await import("node:fs");
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/library.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ async function runNodeScript(
SENTRY_CLI_NO_TELEMETRY: "1",
};
// Ensure no auth leaks into tests — delete rather than set to undefined
// because Bun.spawn may pass "undefined" as a literal string
// because spawn may pass "undefined" as a literal string
delete env.SENTRY_AUTH_TOKEN;
delete env.SENTRY_TOKEN;

Expand Down
Loading
Loading