Skip to content
Closed
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
16 changes: 16 additions & 0 deletions .changeset/ai-bedrock-static-bundler-specifiers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@tanstack/ai-bedrock": patch
---

Use string-literal specifiers for the AWS SDK dynamic imports in
`@tanstack/ai-bedrock` so static bundlers β€” esbuild, bun build, Rollup β€” can
resolve them. Previously the adapter loaded `@aws-sdk/client-bedrock-runtime`
and `@aws-sdk/credential-providers` through `import(/* @vite-ignore */ mod)`
where `mod` was a variable; static analysers cannot resolve a non-literal
specifier, so the SDK was left external and runtime resolution failed in
`node_modules`-free server bundles. Switching to `import('@aws-sdk/...')`
keeps the dynamic import (so the Node-only SDK still defers until first use)
while letting bundlers include it in self-contained server artifacts. Adds a
source-level regression test pinning the literal-specifier contract.

Fixes #929.
26 changes: 17 additions & 9 deletions packages/ai-bedrock/src/adapters/converse-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,25 @@ export class BedrockConverseTextAdapter<
}

/**
* Dynamically import `@aws-sdk/client-bedrock-runtime`. The specifier is held
* in a variable (not a string literal) so bundler dep scanners (e.g. Vite/
* esbuild optimizeDeps) cannot statically discover the AWS SDK and try to
* pre-bundle it for the browser β€” it would fail on the SDK's Node-only
* `fromTokenFile` export chain. The SDK is Node/server-only and is only
* reached on a real request. `typeof import(...)` is a type-only reference
* (erased at emit) so the imported members keep full typing.
* Dynamically import `@aws-sdk/client-bedrock-runtime`. The string-literal
* specifier lets static bundlers β€” esbuild, bun build, Rollup β€” resolve and
* include the AWS SDK in self-contained server bundles (#929). The SDK is
* Node/server-only and is only reached on a real request, so the dynamic
* import also keeps it out of the module-load graph until first use.
* `typeof BedrockRuntime` (from the type-only namespace import above) is
* erased at emit, so the imported members keep full typing β€” no value-level
* `as` cast is needed on the return.
*
* Vite's dev-time optimizeDeps pre-bundler may try to scan this import for
* the browser and fail on the SDK's Node-only exports. Browser-side use of
* this server-only adapter is unsupported; if Vite flags the SDK during a
* server build, add `@aws-sdk/client-bedrock-runtime` (and
* `@aws-sdk/credential-providers`) to `optimizeDeps.exclude` β€” see
* `docs/adapters/bedrock.md` and the matching pattern in
* `examples/ts-react-chat/vite.config.ts`.
*/
protected importBedrockRuntime(): Promise<typeof BedrockRuntime> {
const mod = '@aws-sdk/client-bedrock-runtime'
return import(/* @vite-ignore */ mod) as Promise<typeof BedrockRuntime>
return import('@aws-sdk/client-bedrock-runtime')
}

/**
Expand Down
24 changes: 15 additions & 9 deletions packages/ai-bedrock/src/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,23 @@ export function resolveBedrockAuth(
kind: 'sigv4',
region,
service: sigv4Service(endpoint),
// Lazy credential provider: the AWS SDK is Node/server-only, so we defer the
// dynamic import until SigV4 actually needs to resolve credentials. The
// specifier is held in a variable (not a string literal) so bundler dep
// scanners (e.g. Vite/esbuild optimizeDeps) cannot statically discover the
// AWS SDK and try to pre-bundle it for the browser β€” it would fail on the
// SDK's Node-only `fromTokenFile` export chain. `typeof import(...)` is a
// type-only reference (erased at emit) so we keep full typing.
// Lazy credential provider: the AWS SDK is Node/server-only, so we defer
// the dynamic import until SigV4 actually needs to resolve credentials.
// A string-literal specifier (rather than a variable) lets static bundlers
// β€” esbuild, bun build, Rollup β€” resolve and include the SDK in
// self-contained server bundles (#929). The SDK still stays out of the
// module-load graph until first use because the import is dynamic.
//
// Vite's dev-time optimizeDeps pre-bundler may try to scan this import
// for the browser and fail on the SDK's Node-only `fromTokenFile` export
// chain. Browser-side use of this server-only adapter is unsupported; if
// Vite flags the SDK during a server build, add `@aws-sdk/credential-providers`
// (and `@aws-sdk/client-bedrock-runtime`) to `optimizeDeps.exclude` β€” see
// `docs/adapters/bedrock.md` and the matching pattern in
// `examples/ts-react-chat/vite.config.ts`.
credentials: async (...args) => {
const mod = '@aws-sdk/credential-providers'
const { fromNodeProviderChain } = (await import(
/* @vite-ignore */ mod
'@aws-sdk/credential-providers'
)) as typeof CredentialProviders
return fromNodeProviderChain()(...args)
},
Expand Down
73 changes: 73 additions & 0 deletions packages/ai-bedrock/tests/bundler-static-analysis.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'

/**
* Regression test for issue #929: `@tanstack/ai-bedrock` used to load its AWS
* SDK dependencies through dynamic imports whose specifier was a *variable*
* β€” `const mod = '@aws-sdk/...'; import(mod)` with a leading @vite-ignore
* comment. Static bundlers β€” esbuild, bun build, Rollup β€” cannot resolve
* `import()` when the argument is not a string literal, so the SDK was left
* external and runtime resolution failed in `node_modules`-free server
* bundles.
*
* The fix is to use string-literal specifiers (`import('@aws-sdk/...')`). This
* test pins the contract: every AWS SDK dynamic import in the adapter must use
* a string-literal specifier. If a future refactor brings the variable-specifier
* pattern back, this test fails before the regression ships.
*
* Why a source-level check (not a real bundler run): the contract is purely
* about static analysability β€” whether the specifier is a literal or not. A
* regex over the source is the smallest test that faithfully pins it, with no
* new dev dependency (esbuild/acorn/etc.) and no slow build step in the unit
* suite. The behaviour is unchanged at runtime; only the static shape matters.
*/
describe('AWS SDK dynamic imports β€” static-bundler resolvable (#929)', () => {
const cases = [
{
file: 'src/utils/auth.ts',
sdk: '@aws-sdk/credential-providers',
// The credential-providers import is inside an async lambda inside
// resolveBedrockAuth's sigv4 branch β€” assert the literal import is
// present at all (the anti-pattern check below guards the regression).
},
{
file: 'src/adapters/converse-text.ts',
sdk: '@aws-sdk/client-bedrock-runtime',
},
] as const

for (const { file, sdk } of cases) {
describe(`${file} β†’ ${sdk}`, () => {
const path = fileURLToPath(new URL(`../${file}`, import.meta.url))
const source = readFileSync(path, 'utf8')

it('imports the SDK via a string-literal specifier', () => {
// Required shape: `import('@aws-sdk/...')` β€” single or double quotes.
// Matches `import('@aws-sdk/foo')`, `import("@aws-sdk/foo")`, and
// whitespace variations like `import( '@aws-sdk/foo' )`.
const literalPattern = new RegExp(
`import\\s*\\(\\s*['"]${escapeRegex(sdk)}['"]\\s*\\)`,
)
expect(literalPattern.test(source)).toBe(true)
})

it('does not use the variable-specifier anti-pattern', () => {
// Anti-pattern: `const mod = '@aws-sdk/...'; ... import(/* @vite-ignore */ mod)`
// β€” the variable defeats static analysers. Pin both halves: the
// variable assignment and the `@vite-ignore`-commented `import(mod)`.
const variableAssignment = new RegExp(
`const\\s+mod\\s*=\\s*['"]${escapeRegex(sdk)}['"]`,
)
const viteIgnoredModImport = /import\s*\(\s*\/\*\s*@vite-ignore\s*\*\/\s*mod\s*\)/
expect(variableAssignment.test(source)).toBe(false)
expect(viteIgnoredModImport.test(source)).toBe(false)
})
})
}
})

/** Escape RegExp metacharacters in a package specifier (`@`/`/` are safe). */
function escapeRegex(s: string): string {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}