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
101 changes: 97 additions & 4 deletions .lore.md

Large diffs are not rendered by default.

26 changes: 25 additions & 1 deletion script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ const VERSION: string = pkg.version;
/** Pin to Node 22 LTS for SEA binaries */
const NODE_VERSION = "22";

/** Files that use _require() for lazy relative imports (circular dep breaking). */
const REQUIRE_ALIAS_FILTER =
/(?:db[\\/](?:index|schema)|list-command|telemetry)\.ts$/;
const REQUIRE_ALIAS_RE = /\b_require\(/g;

/** Build-time constants injected into the binary */
const SENTRY_CLIENT_ID = process.env.SENTRY_CLIENT_ID ?? "";

Expand Down Expand Up @@ -140,7 +145,26 @@ async function bundleJs(): Promise<boolean> {
"process.env.NODE_ENV": JSON.stringify("production"),
__SENTRY_DEBUG_ID__: JSON.stringify(PLACEHOLDER_DEBUG_ID),
},
plugins: [textImportPlugin],
plugins: [
textImportPlugin,
// Transform _require() → require() so esbuild resolves lazy relative
// requires at bundle time. In tsx dev mode, _require is a file-local
// createRequire(import.meta.url) that resolves relative to the file.
// esbuild only statically resolves bare require() calls.
// Only targets the specific files that use _require with relative paths.
{
name: "require-alias",
setup(b) {
b.onLoad({ filter: REQUIRE_ALIAS_FILTER }, async (args) => {
const source = await readFile(args.path, "utf-8");
return {
contents: source.replace(REQUIRE_ALIAS_RE, "require("),
loader: args.path.endsWith(".tsx") ? "tsx" : "ts",
};
});
},
},
],
});

const output = result.metafile?.outputs[BUNDLE_JS];
Expand Down
25 changes: 24 additions & 1 deletion script/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,30 @@ const sentrySourcemapPlugin: Plugin = {
};

// Always inject debug IDs (even without auth token); upload is gated inside the plugin
const plugins: Plugin[] = [sentrySourcemapPlugin, textImportPlugin];
/** Files that use _require() for lazy relative imports (circular dep breaking). */
const REQUIRE_ALIAS_FILTER =
/(?:db[\\/](?:index|schema)|list-command|telemetry)\.ts$/;
const REQUIRE_ALIAS_RE = /\b_require\(/g;

/** Transform _require() → require() so esbuild resolves lazy relative requires. */
const requireAliasPlugin: Plugin = {
name: "require-alias",
setup(b) {
b.onLoad({ filter: REQUIRE_ALIAS_FILTER }, async (args) => {
const source = await readFile(args.path, "utf-8");
return {
contents: source.replace(REQUIRE_ALIAS_RE, "require("),
loader: args.path.endsWith(".tsx") ? "tsx" : "ts",
};
});
},
};

const plugins: Plugin[] = [
sentrySourcemapPlugin,
textImportPlugin,
requireAliasPlugin,
];

if (process.env.SENTRY_AUTH_TOKEN) {
console.log(" Sentry auth token found, source maps will be uploaded");
Expand Down
26 changes: 8 additions & 18 deletions script/require-shim.mjs
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
/**
* ESM preload shim that provides `require` in ESM modules and handles
* `with { type: "file" }` import attributes in tsx dev mode.
* ESM preload shim for 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
* bundle, but fails under tsx/Node ESM. This shim bridges the gap by making
* `require` globally available via `createRequire`.
* 1. Provides a global `require()` for ESM modules in `"type": "module"`
* packages. Anchored at the project root — works for `node:*` builtins
* and npm packages. Files that need relative `require()` must use a
* file-local `createRequire(import.meta.url)` instead.
*
* 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 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.
* 2. Handles `with { type: "file" }` import attributes that Node.js doesn't
* support natively. Registers 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.
Expand Down
6 changes: 4 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@
*/

import { getEnv } from "./lib/env.js";
import { captureEnvTokenHost } from "./lib/env-token-host.js";
import { CliError } from "./lib/errors.js";
import { applySentryCliRcEnvShim } from "./lib/sentryclirc.js";

/**
* Preload project context: walk up from `cwd` once, finding both the
Expand All @@ -25,6 +23,8 @@ async function preloadProjectContext(cwd: string): Promise<void> {
// Snapshot env-token host BEFORE anything mutates env.SENTRY_HOST/URL
// (the .sentryclirc shim or the default-URL fallback below). Pins the
// env-token's trust scope to the user's shell, not a repo-local file.
// Dynamic import: env-token-host chains into db/auth → telemetry → @sentry/node-core
const { captureEnvTokenHost } = await import("./lib/env-token-host.js");
captureEnvTokenHost();

// Dynamic import keeps the heavy DSN/DB modules out of the completion fast-path
Expand All @@ -42,6 +42,8 @@ async function preloadProjectContext(cwd: string): Promise<void> {
// Apply .sentryclirc env shim (token + URL). The URL trust check is
// deferred to buildCommand's wrapper where commands can opt out via
// skipRcUrlCheck (used by auth login/logout).
// Dynamic import: sentryclirc chains into db/index → sqlite, logger → consola
const { applySentryCliRcEnvShim } = await import("./lib/sentryclirc.js");
await applySentryCliRcEnvShim(cwd);

// Apply persistent URL default (lower priority than env vars and .sentryclirc).
Expand Down
3 changes: 1 addition & 2 deletions src/lib/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +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 {
const { createTracedDatabase } = _require("../telemetry.js") as {
createTracedDatabase: (d: Database) => Database;
};
db = createTracedDatabase(rawDb);
Expand Down
3 changes: 1 addition & 2 deletions src/lib/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -674,8 +674,7 @@ export function tryRepairAndRetry<T>(
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 {
const { getRawDatabase } = _require("./index.js") as {
getRawDatabase: () => Database;
};

Expand Down
3 changes: 1 addition & 2 deletions src/lib/list-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,8 +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 {
const { routes } = _require("../app.js") as {
routes: { getAllEntries: () => readonly RouteEntry[] };
};

Expand Down
3 changes: 1 addition & 2 deletions src/lib/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1021,8 +1021,7 @@ const noop = (): void => {};
/** 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 {
const { getDbPath } = _require("./db/index.js") as {
getDbPath: () => string;
};
return getDbPath();
Expand Down
Loading