diff --git a/.changeset/ai-bedrock-static-bundler-specifiers.md b/.changeset/ai-bedrock-static-bundler-specifiers.md new file mode 100644 index 000000000..68b7c7f58 --- /dev/null +++ b/.changeset/ai-bedrock-static-bundler-specifiers.md @@ -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. diff --git a/packages/ai-bedrock/src/adapters/converse-text.ts b/packages/ai-bedrock/src/adapters/converse-text.ts index 8a2fa8f5a..88b2d1ee8 100644 --- a/packages/ai-bedrock/src/adapters/converse-text.ts +++ b/packages/ai-bedrock/src/adapters/converse-text.ts @@ -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 { - const mod = '@aws-sdk/client-bedrock-runtime' - return import(/* @vite-ignore */ mod) as Promise + return import('@aws-sdk/client-bedrock-runtime') } /** diff --git a/packages/ai-bedrock/src/utils/auth.ts b/packages/ai-bedrock/src/utils/auth.ts index babcd647d..2839c3f2a 100644 --- a/packages/ai-bedrock/src/utils/auth.ts +++ b/packages/ai-bedrock/src/utils/auth.ts @@ -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) }, diff --git a/packages/ai-bedrock/tests/bundler-static-analysis.test.ts b/packages/ai-bedrock/tests/bundler-static-analysis.test.ts new file mode 100644 index 000000000..e71274fca --- /dev/null +++ b/packages/ai-bedrock/tests/bundler-static-analysis.test.ts @@ -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, '\\$&') +}