#6979·sst

Provider-global imports are prepended to `node_modules` sources, breaking packages that declare a matching name

Author: chinigoCreated Aug 22, 2026Updated Aug 22, 2026

Attribution

  • Claude wrote this bug report, everything below this "Attribution" stanza (apart from some minor edits).
  • Regular old human me, Mike Chinigo, reviewed this report in detail. I vouch for it in its entirety.
  • I also verified the linked minimal reproduction manually. Works on my box (i.e. fails as described).
  • My (closed source commercial) repo has a hack fix around this problem, by using pnpm to patch the sql-formatter dependency that causes the namespace conflict.

Thanks! ~Mike

Summary

During the run-phase config build, SST prepends one import * as <provider> from "@pulumi/<provider>" line per configured provider to the source text of every node_modules module. Any ESM module in the dependency graph that declares a top-level binding of the same name — for example sql-formatter, which exports const postgresql — then contains two declarations of that identifier in one scope, and esbuild rejects the build.

Minimal reproduction

https://github.com/chinigo/sst-provider-global-collision

Steps to reproduce

Full reproduction, including a repro.sh that runs the whole sequence: https://github.com/chinigo/sst-provider-global-collision

bash
git clone https://github.com/chinigo/sst-provider-global-collision
cd sst-provider-global-collision
./repro.sh

Equivalently, by hand:

bash
npm install
npx sst install                  # succeeds
npx sst deploy --stage repro     # fails while bundling

The reproduction is three ingredients:

  1. providers: { postgresql: "..." } in sst.config.tsone provider is enough.
  2. A dependency whose ESM build declares a top-level binding named postgresql. The repro uses [email protected], whose dist/esm/languages/postgresql/postgresql.formatter.js has export const postgresql = { at line 261.
  3. run() reaching that dependency. (It must be pulled in via await import(), since SST rejects top-level imports in sst.config.ts.)

Expected behavior

sst deploy bundles the config successfully. A third-party package's choice of top-level identifiers is its own business, and should not have to avoid the names of the Pulumi providers the consuming app happens to configure.

Actual behavior

The build fails:

|  Error       node_modules/sql-formatter/dist/esm/languages/postgresql/postgresql.formatter.js:263:13: The symbol "postgresql" has already been declared
✕  Unexpected error occurred. Please run with --print-logs or check .sst/log/sst.log if available.

.sst/log/sst.log shows the config being built twice, with only the second build failing:

level=INFO  msg="esbuild building" out=.../.sst/platform/sst.config.1787433733959.mjs
level=INFO  msg="esbuild built"    outfile=.../.sst/platform/sst.config.1787433733959.mjs
level=INFO  msg="evaluating config"
level=INFO  msg="config evaluated"
level=INFO  msg="esbuild building" out=.../.sst/platform/sst.config.1787433734009.mjs
level=ERROR msg="esbuild error" text="The symbol \"postgresql\" has already been declared"

The reported line number is the module's own declaration shifted by the number of injected lines. Capturing the generated sst.config.*.mjs from a passing run (by patching the offending package) shows the injected imports, one copy per third-party module — 683 copies in our real application, each renamed by esbuild in the output:

javascript
// ../node_modules/sql-formatter/dist/esm/languages/postgresql/postgresql.formatter.js
import * as rabbitmq633 from "@pulumi/rabbitmq";
import * as postgresql635 from "@pulumi/postgresql";
...

Additional observations

These may help narrow it down:

  • Only the run-phase build injects. sst install succeeds on the same tree, because it stops after the first build. sst diff fails identically once the stage exists; on a fresh stage it only appears to pass because it exits at Stage not found before reaching the second build.
  • First-party modules are not injected. An identical export const postgresql = { ... } in a first-party file, imported by run() in the same failing build, never collides. Only node_modules sources are affected.
  • CJS dependencies are unaffected, since esbuild wraps them in a function scope — the collision needs an ESM module whose top-level bindings share the output scope.
  • The offset is exactly providers + 1. With 1 provider the error moves to line 263, with 3 to line 265; our 8-provider application reported line 270 for the same declaration at line 261. That is consistent with one line per provider plus one sst import.
  • The name space is the provider list, so the exposure is not limited to postgresql: aws, command, random, grafana, rabbitmq, auth0 and dockerbuild are all plausible top-level identifiers in published packages.

Proposed fix

Use esbuild's inject option instead of prepending import statements to module source text. inject exists for exactly this purpose and is collision-safe by design: esbuild skips injection for any module that already declares the name, rather than producing a duplicate declaration.

We verified this directly. Bundling an entry that imports sql-formatter, with a shim exporting postgresql supplied via --inject:shim.js, builds cleanly — while concatenating the same provider imports onto the module's source reproduces the reported error at the reported line and column.

Two properties would follow:

  • Packages could no longer break a build by choosing an identifier that matches a provider name.
  • Reported error positions would stop being offset relative to the real file, which is what made this hard to diagnose — the cited line does not exist in the package as published.

If source-text prepending has to stay, restricting it to first-party sources would also resolve it, since first-party modules are the ones that actually reference the provider globals.

Environment

SST 4.1.0
esbuild (bundled in .sst/platform) 0.21.5
Node 24.14.0
Package manager pnpm 11.22.0 (repro repo uses npm)
OS macOS 26.6.2 arm64; also reproduced on GitHub Actions ubuntu-latest x86_64
Offending package [email protected]