Skip to content

fix(js-sdk): unpin useDefineForClassFields — make caller-directory resolution emit-invariant - #1539

Merged
mishushakov merged 1 commit into
mainfrom
fix-usedefineforclassfields
Jul 14, 2026
Merged

fix(js-sdk): unpin useDefineForClassFields — make caller-directory resolution emit-invariant#1539
mishushakov merged 1 commit into
mainfrom
fix-usedefineforclassfields

Conversation

@mishushakov

Copy link
Copy Markdown
Member

Follow-up to #1536, which pinned useDefineForClassFields: false in the js-sdk tsconfig because raising target to es2022 flips the default to true, and that broke the template builder. This PR fixes the root cause and removes the pin, so the SDK now compiles with the standard es2022 [[Define]] class-field semantics.

Root cause

TemplateBase resolved its default fileContextPath in a class field initializer:

private fileContextPath: PathLike =
  runtime === 'browser' ? '.' : (getCallerDirectory(STACK_TRACE_DEPTH) ?? '.')

With native class fields (define semantics), V8 evaluates field initializers in an extra <instance_members_initializer> stack frame:

at getCallerDirectory (utils.ts)
at <instance_members_initializer> (index.ts)   ← extra frame under define semantics
at new TemplateBase (index.ts)
at Template (index.ts)
at user code                                    ← fixed-depth walk lands one frame short

getCallerDirectory walks the stack at a fixed depth, so it landed on the SDK's own src/template directory instead of the caller's — .copy('folder/*', …) then globbed against the wrong base dir (Error: No files found in .../src/template/...), and the resulting client-side failure mis-attributed build-step stack traces (the two stacktrace.test.ts failures were cascades of this one bug).

Fix

Move the default resolution into the constructor body, where the stack shape is identical under both emits:

constructor(options?: TemplateOptions) {
  this.fileContextPath =
    options?.fileContextPath ??
    (runtime === 'browser' ? '.' : (getCallerDirectory(STACK_TRACE_DEPTH) ?? '.'))

The call is now emit-invariant (same STACK_TRACE_DEPTH), so the tsconfig pin is removed. The method-level getCallerFrame call sites were never affected — method bodies don't change shape with class-field semantics.

Only the js-sdk is touched: the Python SDKs resolve the caller via inspect and don't have this failure mode, and the CLI bundle doesn't include TemplateBase.

Usage example

Fixes relative-path resolution for SDK consumers whose toolchain emits native class fields (e.g. esbuild/vitest with target: es2022+):

// user-project/scripts/template.ts
const template = Template()
  .fromBaseImage()
  .copy('assets/*', '/app/assets') // now resolves against user-project/scripts/,
                                   // not the SDK's own directory

Verification

  • tests/template/stacktrace.test.ts — 30/30 pass with the flag defaulted (true), and still 30/30 when explicitly set back to false (emit-invariance)
  • tests/template/build.test.ts — 4/4 pass against the real backend (real .copy glob + build)
  • Smoke-tested built dist/index.mjs and dist/index.js from an external directory: fileContextPath resolves to the importing script's directory in both
  • Unit project A/B: identical results with and without this change (remaining failures are pre-existing E2B_API_KEY-gated live tests)
  • pnpm run typecheck, lint, format

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 75c63ed

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
e2b Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches how relative copy paths and file hashing resolve for all Node template builds, but the change is localized and targets a known stack-trace bug.

Overview
With target: es2022, default [[Define]] class-field emit added an extra stack frame when getCallerDirectory ran in a field initializer, so the template builder’s default fileContextPath pointed at the SDK instead of the caller and relative .copy() paths failed.

Default resolution now runs in the TemplateBase constructor (still honoring fileContextPath in options), which keeps stack depth stable across emits. The useDefineForClassFields: false workaround was removed from packages/js-sdk/tsconfig.json. A patch changeset documents the fix.

Reviewed by Cursor Bugbot for commit 75c63ed. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from 1377cc2. Download artifacts from this workflow run.

JS SDK (e2b@2.32.1-fix-usedefineforclassfields.0):

npm install ./e2b-2.32.1-fix-usedefineforclassfields.0.tgz

CLI (@e2b/cli@2.13.2-fix-usedefineforclassfields.0):

npm install ./e2b-cli-2.13.2-fix-usedefineforclassfields.0.tgz

Python SDK (e2b==2.31.0+fix.usedefineforclassfields):

pip install ./e2b-2.31.0+fix.usedefineforclassfields-py3-none-any.whl

Resolve the template builder's default fileContextPath in the constructor
body instead of a class field initializer. With [[Define]] class-field
semantics (the default at target es2022+), field initializers run in an
extra <instance_members_initializer> stack frame, shifting the fixed-depth
stack walk in getCallerDirectory by one — .copy() sources resolved against
the SDK's own directory instead of the caller's. The constructor-body call
is emit-invariant, so the useDefineForClassFields: false pin in the js-sdk
tsconfig is no longer needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mishushakov
mishushakov force-pushed the fix-usedefineforclassfields branch from 9cd65a4 to 75c63ed Compare July 9, 2026 18:44
@mishushakov
mishushakov enabled auto-merge (squash) July 9, 2026 18:45
@mishushakov
mishushakov merged commit 64e9bc0 into main Jul 14, 2026
26 of 28 checks passed
@mishushakov
mishushakov deleted the fix-usedefineforclassfields branch July 14, 2026 12:01
mishushakov added a commit that referenced this pull request Jul 24, 2026
…depth (#1599)

## Description

Template build stack traces were captured by walking a fixed number of
frames (`STACK_TRACE_DEPTH` plus `±1` arithmetic at ~15 call sites),
which broke whenever the frame count between `new Error()` and user code
shifted — TS class-field initializer frames (#1539) and Bun's tail-call
frame elision were both this bug. This PR makes two related changes:

1. **Boundary-based frame selection.** The caller's frame is now the
first one whose file lies outside the SDK package, making extra
transpiler frames and elided delegating frames irrelevant. In the JS
SDK, frame parsing is delegated to `error-stack-parser-es` (ESM-only, so
it's a devDependency inlined into both dist formats via tsdown
`noExternal` — the engines range includes Node versions without
`require(esm)`); the Python SDK equivalently walks `f_back` until
`co_filename` leaves the `e2b` package root, in the shared builder used
by both sync and async. If no user frame is identifiable (e.g. the SDK
is bundled into the caller's own file), capture degrades to no trace
rather than a wrong frame.
2. **Dead machinery removed.** Because boundary capture resolves through
SDK-internal delegation (`remove()` → `runCmd()`, `fromDockerfile()` →
parser) to the user's call site on its own, the suppress/override
collection machinery (`runInNewStackTraceContext`,
`runInStackTraceOverrideContext`, the enabled/override flags, and their
Python equivalents) became redundant and is removed — superseding the
approach in #1596.

Error `.stack` synthesis (keeping the `Name: message` header and the
throw site on `cause`) was prototyped here and backed out — it will come
as a follow-up PR.

## Usage

No API changes — build errors now point at the user's call site
regardless of runtime or transpiler:

```ts
const template = Template()
  .fromBaseImage()
  .runCmd('./does-not-exist') // ← build failures point exactly here

await Template.build(template, 'my-template')
```

## Testing

- JS: `unit` + `template` vitest projects green against the real API
(incl. 27 per-method stacktrace tests pinning exact call-site
line/columns, `bunInstall` now covered); edge-compat bundle test and CLI
build verified; built CJS/ESM dists smoke-tested with
`require()`/`import()`.
- Python: all 184 template tests green (shared + sync + async, incl.
both `test_stacktrace.py` suites, `bun_install` now covered); `ruff` and
`ty` clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants