aws / aws/aws-cdk

(aws-lambda-nodejs): PackageInstallation.detect() fails to find esbuild in npm workspaces when not hoisted to root

Open
#37,545 2 comments 0 reactions 0 assignees View on GitHub
@aws-cdk/aws-lambda-nodejs effort/small p1
Dominant language
TypeScript
Stars
12.9k
Forks
4.6k
Avg merge
2d 3h
Merged PRs (30d)
83

Description

### Describe the bug

`PackageInstallation.detect("esbuild")` uses `require("esbuild/package.json")` to detect whether esbuild is installed locally. Because `require()` resolves from the calling module's location (`node_modules/aws-cdk-lib/aws-lambda-nodejs/lib/util.js`), it can only find esbuild at the root `node_modules/esbuild/`. It cannot find esbuild installed in workspace-local `node_modules/` directories (e.g., `packages/my-app/node_modules/esbuild/`).

This means `NodejsFunction` silently falls back to Docker-based bundling in npm workspace monorepos whenever esbuild is not hoisted to root — even though esbuild is installed and fully functional.

Whether esbuild ends up at root or in workspace-local dirs depends on npm's hoisting algorithm, which is influenced by other packages in the dependency tree. For example, if another root-level dependency happens to also depend on esbuild with a compatible semver range, npm will hoist it. If that transitive dependency is removed or its semver range becomes incompatible (as happens with `0.x` packages where `^0.27.0` excludes `0.28.0`), npm stops hoisting and detection breaks — with no code changes on the user's part.

**Impact:** In CI environments, the silent Docker fallback often fails with architecture mismatches (`exec format error` on ARM runners pulling x86 images), causing cryptic build failures that are difficult to diagnose since the root cause (esbuild not detected) is hidden behind the fallback behavior.

### Regression Issue

- [x] Select this option if this issue appears to be a regression.

### Last Known Working CDK Library Version

2.245.0

### Expected Behavior

`PackageInstallation.detect("esbuild")` should find esbuild when it's installed in the project, regardless of whether npm hoisted it to root or placed it in a workspace-local `node_modules/` directory.

### Current Behavior

Detection fails when esbuild is in a workspace-local `node_modules/` and not at root. CDK prints `esbuild cannot run locally. Switching to Docker bundling.` and falls back to container-based builds, even though esbuild is installed and the binary works.

### Reproduction Steps

```bash
mkdir repro && cd repro

# Create monorepo structure
cat > package.json << 'INNER'
{
"name": "repro",
"private": true,
"workspaces": ["packages/*"]
}
INNER

mkdir -p packages/my-app

cat > packages/my-app/package.json << 'INNER'
{
"name": "my-app",
"dependencies": {
"aws-cdk-lib": "^2.248.0",
"constructs": "^10.0.0",
"esbuild": "^0.28.0"
}
}
INNER

npm install

# esbuild is in packages/my-app/node_modules/esbuild, NOT at root
ls node_modules/esbuild 2>&1 || echo "Not at root"
ls packages/my-app/node_modules/esbuild

# CDK's detection fails:
node -e "
const Module = require('module');
const path = require('path');
const cdkRequire = Module.createRequire(
path.join(process.cwd(), 'node_modules/aws-cdk-lib/aws-lambda-nodejs/lib/util.js')
);
try {
console.log('Found:', cdkRequire('esbuild/package.json').version);
} catch(e) {
console.log('Detection FAILED:', e.code);
}
"
```

### Possible Solution

Change `tryGetModuleVersionFromRequire` in `util.ts` to resolve esbuild relative to `process.cwd()` instead of from CDK's own module location. For example:

```typescript
export function tryGetModuleVersionFromRequire(mod: string): string | undefined {
try {
const req = Module.createRequire(path.join(process.cwd(), 'package.json'));
return req(`${mod}/package.json`).version;
} catch (err) {
return undefined;
}
}
```

This would find esbuild in workspace-local `node_modules/` directories by starting resolution from the project root where `cdk synth` is invoked.

### Additional Information/Context

This affects any npm workspace monorepo where esbuild is declared as a dependency in specific workspaces rather than at the root. The current workaround is adding esbuild as a `devDependency` in the root `package.json`, but this forces a project-wide dependency that may only be needed by specific workspaces.

We discovered this after Vite 8 moved esbuild from `dependencies` to an optional `peerDependencies` (as part of their switch to Rolldown), which removed esbuild as a transitive root-level dependency in our monorepo. However, the underlying issue affects any monorepo where npm's hoisting algorithm places esbuild in workspace-local dirs rather than root — which can happen for various reasons including semver range conflicts between packages.

Note: PR #37292 (merged Mar 25, 2026) improved how CDK *executes* esbuild (direct spawn vs shell), but did not change how CDK *detects* esbuild — `PackageInstallation.detect()` still uses the same `require()` call.

See aws-powertools/powertools-lambda-typescript#5172 for more context and a setup that exhibits the issue [at this commit sha](https://github.com/aws-powertools/powertools-lambda-typescript/tree/364411bdbbbfea428b27f399953486ad1553a3c1).

### AWS CDK Library version (aws-cdk-lib)

2.248.0

### AWS CDK CLI version

2.1113.0

### Node.js Version

v24.11.1

### OS

macOS / Ubuntu (reproducible on both)

### Language

- [x] TypeScript

### Other information

_No response_

Contributor guide

Open the contributing guide

Research direction

Start in util.ts at tryGetModuleVersionFromRequire and trace how PackageInstallation.detect() resolves esbuild. Reproduce the npm workspace layout from the issue, then run the relevant aws-lambda-nodejs tests or CDK bundling flow. Done means locally installed workspace esbuild is detected and NodejsFunction no longer falls back to Docker.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript
Domain
build-system, tooling
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.