aws / aws/aws-cdk

(aws-lambda-nodejs): NodejsFunction bundling fails in pnpm workspaces using catalogs

Open
#38,029 3 comments 0 reactions 0 assignees View on GitHub
@aws-cdk/aws-lambda-nodejs effort/medium p2
Dominant language
TypeScript
Stars
12.9k
Forks
4.6k
Avg merge
1d 19h
Merged PRs (30d)
71

Description

### Describe the feature

`NodejsFunction` (`aws-cdk-lib/aws-lambda-nodejs`) currently can't bundle Lambdas in a pnpm workspace that uses [pnpm catalogs](https://pnpm.io/catalogs) when `bundling.nodeModules: [...]` is set.

Pnpm catalogs let monorepos pin shared dependency versions in `pnpm-workspace.yaml` and reference them from `package.json` via the `catalog:` protocol:

```jsonc
// apps/my-lambda/package.json
{ "dependencies": { "@sparticuz/chromium": "catalog:" } }
```

When CDK bundles such a Lambda with `bundling.nodeModules: ['@sparticuz/chromium']`, it generates a bash command roughly like:

```sh
esbuild ... \
&& [commandHooks.beforeInstall output, if any] \
&& echo '' > /pnpm-workspace.yaml \
&& echo '{"dependencies":{"@sparticuz/chromium":"catalog:"}}' > pkg \
&& cp pnpm-lock.yaml ... \
&& cd && pnpm install
```

That `pnpm install` fails:

```
[ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC] No catalog entry '@sparticuz/chromium' was found for catalog 'default'.
```

The empty `pnpm-workspace.yaml` that decouples the temp dir from the parent workspace also strips the catalog config (by design per [#21910](https://github.com/aws/aws-cdk/issues/21910)), so pnpm has nothing to resolve `catalog:` against.

### Use Case

Pnpm catalogs are the recommended pnpm pattern for monorepo dependency-version pinning ([pnpm docs](https://pnpm.io/catalogs), used in pnpm's own monorepo). Adoption is growing through 2025–26 as teams centralize version management. Any monorepo using catalogs that also has a CDK Lambda needing `nodeModules` for a native binary (`@sparticuz/chromium`, `sharp`, etc.) or an externally-blocked package hits this dead-end.

There's currently **no working escape hatch** — `catalog:` only resolves inside a workspace context, and CDK's bundling intentionally breaks workspace context.

### Why `commandHooks.beforeInstall` can't be used as a workaround

`commandHooks.beforeInstall` fires **before** the install command, but CDK's install command writes the empty `pnpm-workspace.yaml` as its *first step*. So any catalog yaml a user writes from `beforeInstall` gets immediately overwritten on the very next `&&` link in the bash chain — before pnpm install runs.

I attempted to ship `commandHooks.beforeInstall` returning `echo | base64 -d > pnpm-workspace.yaml` in a real monorepo. The hook fires, writes the file, then the next command in the chain (`echo '' > pnpm-workspace.yaml`) blanks it. Pnpm install reads the empty file and fails with `ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC` exactly as if no hook were configured.

There is no hook between CDK's install-prep steps and the actual `pnpm install`.

### Workarounds available today (all bad)

1. Pin the affected dep to a concrete version in the project's `package.json`, sidestepping `catalog:` for that dep — defeats the catalog migration for any dep needed by a `nodeModules` Lambda.
2. Drop `nodeModules` and let esbuild bundle the dep — impossible for native binaries like `@sparticuz/chromium`.
3. Move the dep to a Lambda Layer — significant refactor (separate stack, IAM, ARN wiring, code changes).

### Proposed Solution

Replace (or precede) the `echo '' > pnpm-workspace.yaml` step in CDK's install command with user-supplied content. Two options:

1. **Explicit prop:** add `bundling.pnpmWorkspaceYaml?: string` to `NodejsFunctionProps`. When set, CDK writes that content as the temp `pnpm-workspace.yaml` instead of an empty file. Users supply a minimal yaml containing only the catalog config:

```ts
new NodejsFunction(this, 'fn', {
entry: 'src/handler.ts',
bundling: {
nodeModules: ['@sparticuz/chromium'],
pnpmWorkspaceYaml: readFileSync('pnpm-workspace.yaml', 'utf8'),
},
});
```

2. **Auto-detect:** if the resolved `depsLockFilePath` lives alongside a `pnpm-workspace.yaml` containing a `catalog:` block, copy that catalog block (plus `packages: []`) into the bundling dir by default.

Either would replace the unconditional empty-yaml write with one that respects catalog config. The implementation is small on CDK's side — it's the same `writeFile` step that exists today, just sourcing its content differently.

### Related

- [aws/aws-cdk#21910](https://github.com/aws/aws-cdk/issues/21910) — original "nodeModules with pnpm" issue; explains the empty-yaml design.
- [pnpm/pnpm#10594](https://github.com/pnpm/pnpm/issues/10594) — `ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC` with `dlx`; same root-cause family.
- [pnpm discussion #8600](https://github.com/orgs/pnpm/discussions/8600) — pnpm maintainers confirm `catalog:` is workspace-scoped with no `--catalog=path` flag.

### Acknowledgements

- [x] I may be able to implement this feature request
- [ ] This feature might incur a breaking change

### CDK version used

aws-cdk-lib 2.243.0

### Environment details (OS name and version, etc.)

Node 24, pnpm 11.4, macOS / GitHub Actions Ubuntu

Contributor guide

Open the contributing guide

Research direction

Start at the NodejsFunction bundling logic that writes the temporary pnpm-workspace.yaml and runs pnpm install; read the related empty-workspace design in issue #21910. Trace how depsLockFilePath and bundling options reach that command, then verify that catalog configuration survives into the temporary workspace and pnpm install succeeds.

Written by the indexing model from the issue text.

Assessment

Tech stack
aws, node.js, typescript
Domain
build-system, infrastructure
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.