Skip to content

Commit 4268859

Browse files
tianzhouclaude
andauthored
feat: make cloud provider packages optional (#298)
* feat: make cloud provider packages optional Move @aws-sdk/rds-signer and @azure/identity from dependencies to optionalDependencies and switch to dynamic import() so they are only loaded when AWS IAM or Azure AD auth is actually used. Also add both to tsup external list to prevent bundling. Closes #295 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address review: update tsup comment and add clear errors for missing cloud packages - Update tsup external comment to mention cloud auth packages - Catch import failures for @aws-sdk/rds-signer and @azure/identity and rethrow with actionable install instructions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address review: selective error handling and test for missing cloud packages - Only translate ERR_MODULE_NOT_FOUND into install hint, rethrow other errors so real SDK failures are not masked (reuses isDriverNotInstalled) - Use pnpm-appropriate install command in error messages - Refine tsup comment to distinguish CJS driver externals from cloud auth bundle-size externals - Add test verifying isDriverNotInstalled works with scoped package names Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Clean up test: remove unused imports, fix suite name Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix: let missing-package error bypass Azure AD token error wrapping Move the dynamic import of @azure/identity outside the token-fetching try/catch so the install-instruction error propagates directly instead of being wrapped as "Failed to get Azure AD token: ..." Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bbaa71a commit 4268859

5 files changed

Lines changed: 56 additions & 8 deletions

File tree

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@
4040
"author": "",
4141
"license": "MIT",
4242
"dependencies": {
43-
"@aws-sdk/rds-signer": "^3.1001.0",
44-
"@azure/identity": "^4.8.0",
4543
"@iarna/toml": "^2.2.5",
4644
"@modelcontextprotocol/sdk": "^1.25.1",
4745
"dotenv": "^16.4.7",
@@ -51,6 +49,8 @@
5149
"zod": "^3.24.2"
5250
},
5351
"optionalDependencies": {
52+
"@aws-sdk/rds-signer": "^3.1001.0",
53+
"@azure/identity": "^4.8.0",
5454
"better-sqlite3": "^11.9.0",
5555
"mariadb": "^3.4.0",
5656
"mssql": "^11.0.1",

src/connectors/sqlserver/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
ExecuteOptions,
1212
ConnectorConfig,
1313
} from "../interface.js";
14-
import { DefaultAzureCredential } from "@azure/identity";
14+
import { isDriverNotInstalled } from "../../utils/module-loader.js";
1515
import { SafeURL } from "../../utils/safe-url.js";
1616
import { obfuscateDSNPassword } from "../../utils/dsn-obfuscate.js";
1717
import { SQLRowLimiter } from "../../utils/sql-row-limiter.js";
@@ -94,6 +94,17 @@ export class SQLServerDSNParser implements DSNParser {
9494
// Handle authentication types
9595
switch (options.authentication) {
9696
case "azure-active-directory-access-token": {
97+
let DefaultAzureCredential: typeof import("@azure/identity")["DefaultAzureCredential"];
98+
try {
99+
({ DefaultAzureCredential } = await import("@azure/identity"));
100+
} catch (importError) {
101+
if (isDriverNotInstalled(importError, "@azure/identity")) {
102+
throw new Error(
103+
'Azure AD authentication requires the "@azure/identity" package. Install it with: pnpm add @azure/identity'
104+
);
105+
}
106+
throw importError;
107+
}
97108
try {
98109
const credential = new DefaultAzureCredential();
99110
const token = await credential.getToken("https://database.windows.net/");
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { isDriverNotInstalled } from '../module-loader.js';
3+
4+
describe('isDriverNotInstalled with scoped packages', () => {
5+
it('should match ERR_MODULE_NOT_FOUND for @aws-sdk/rds-signer', () => {
6+
const err = new Error(
7+
"Cannot find package '@aws-sdk/rds-signer' imported from /fake/path"
8+
);
9+
(err as NodeJS.ErrnoException).code = 'ERR_MODULE_NOT_FOUND';
10+
11+
expect(isDriverNotInstalled(err, '@aws-sdk/rds-signer')).toBe(true);
12+
});
13+
14+
it('should not match unrelated ERR_MODULE_NOT_FOUND errors', () => {
15+
const err = new Error(
16+
"Cannot find package 'some-other-pkg' imported from /fake/path"
17+
);
18+
(err as NodeJS.ErrnoException).code = 'ERR_MODULE_NOT_FOUND';
19+
20+
expect(isDriverNotInstalled(err, '@aws-sdk/rds-signer')).toBe(false);
21+
});
22+
});

src/utils/aws-rds-signer.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Signer } from "@aws-sdk/rds-signer";
1+
import { isDriverNotInstalled } from "./module-loader.js";
22

33
export interface RdsAuthTokenParams {
44
hostname: string;
@@ -13,6 +13,18 @@ export interface RdsAuthTokenParams {
1313
* (AWS CLI profile, env vars, instance role, etc.).
1414
*/
1515
export async function generateRdsAuthToken(params: RdsAuthTokenParams): Promise<string> {
16+
let Signer: typeof import("@aws-sdk/rds-signer")["Signer"];
17+
try {
18+
({ Signer } = await import("@aws-sdk/rds-signer"));
19+
} catch (error) {
20+
if (isDriverNotInstalled(error, "@aws-sdk/rds-signer")) {
21+
throw new Error(
22+
'AWS IAM authentication requires the "@aws-sdk/rds-signer" package. Install it with: pnpm add @aws-sdk/rds-signer'
23+
);
24+
}
25+
throw error;
26+
}
27+
1628
const signer = new Signer({
1729
hostname: params.hostname,
1830
port: params.port,

tsup.config.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ export default defineConfig({
88
dts: true,
99
clean: true,
1010
outDir: 'dist',
11-
// Database drivers are optionalDependencies loaded at runtime via dynamic
12-
// import(). They must be external so tsup does not bundle their CJS code
13-
// into ESM chunks (which causes "Dynamic require of X is not supported").
14-
external: ['pg', 'mysql2', 'mariadb', 'mssql', 'better-sqlite3'],
11+
// Optional runtime-loaded dependencies (database drivers and cloud auth
12+
// packages) are declared as optionalDependencies and loaded via dynamic
13+
// import(). Database drivers must be external so tsup does not bundle their
14+
// CJS code into ESM chunks (which causes "Dynamic require of X is not
15+
// supported"). Cloud auth packages are externalized to keep their large
16+
// dependency trees out of the bundle.
17+
external: ['pg', 'mysql2', 'mariadb', 'mssql', 'better-sqlite3', '@aws-sdk/rds-signer', '@azure/identity'],
1518
// Copy the employee-sqlite demo data to dist
1619
async onSuccess() {
1720
// Create target directory

0 commit comments

Comments
 (0)