perf(backend): mark AWS SDK clients as optional dependencies (#821)

@aws-sdk/client-ecr and @aws-sdk/client-s3 each pull in dozens of
@smithy/* and middleware-* transitive packages but only fire when an
operator configures an ECR registry or cloud backup respectively. Move
both to optionalDependencies so the package classification matches
their runtime role and operators who never use either feature can run
`npm ci --omit=optional` for a ~150 MB-slimmer image.

The default Dockerfile install (`npm ci --omit=dev`) keeps shipping
the SDKs, so default installs are unchanged. The dynamic imports in
CloudBackupService.loadS3Sdk and RegistryService.fetchEcrToken now
catch a missing-module failure and throw a wrapped Error whose
message names the recovery path (`reinstall without --omit=optional`)
and whose cause propagates the original module-not-found error for
debugging.

Bumps tsconfig.json's target and lib to ES2022 so `new Error(msg,
{ cause })` is typed; Node 25 already supports this at runtime.
This commit is contained in:
Anso
2026-04-28 02:08:39 -04:00
committed by GitHub
parent 14c25a6dbc
commit 04f35fdf22
5 changed files with 153 additions and 13 deletions
+15 -2
View File
@@ -24,7 +24,10 @@ import { isDebugEnabled } from '../utils/debug';
// Cloud backup is opt-in (Skipper+ feature) and the AWS SDK v3 client pulls
// in dozens of @smithy/* and @aws-sdk/middleware-* packages, so installs
// without cloud backup configured pay a real boot-parse cost they never use.
// Lazy-load the SDK on first call and cache it for subsequent requests.
// The package is declared as an optionalDependency: present in the default
// install, but operators who never touch cloud backup can run
// `npm ci --omit=optional` for a slimmer image. Lazy-load the SDK on first
// call and surface a clear error if it has been opt-out-pruned.
type S3Sdk = typeof import('@aws-sdk/client-s3');
type S3Client = InstanceType<S3Sdk['S3Client']>;
@@ -32,7 +35,17 @@ let cachedS3Sdk: S3Sdk | undefined;
async function loadS3Sdk(): Promise<S3Sdk> {
if (!cachedS3Sdk) {
cachedS3Sdk = await import('@aws-sdk/client-s3');
try {
cachedS3Sdk = await import('@aws-sdk/client-s3');
} catch (err) {
throw new Error(
'Cloud backup requires the @aws-sdk/client-s3 package. ' +
'It is shipped by default; if you built this image with ' +
'`npm ci --omit=optional`, reinstall without that flag to ' +
'enable cloud backup.',
{ cause: err },
);
}
}
return cachedS3Sdk;
}
+17 -1
View File
@@ -439,7 +439,23 @@ export class RegistryService {
secretAccessKey: string,
region: string,
): Promise<EcrCacheEntry> {
const { ECRClient, GetAuthorizationTokenCommand } = await import('@aws-sdk/client-ecr');
// @aws-sdk/client-ecr is declared as an optionalDependency: shipped
// in the default install, but absent when the image was built with
// `npm ci --omit=optional`. Surface a clear error rather than the
// raw "Cannot find module" so operators know how to recover.
let sdk: typeof import('@aws-sdk/client-ecr');
try {
sdk = await import('@aws-sdk/client-ecr');
} catch (err) {
throw new Error(
'ECR registry support requires the @aws-sdk/client-ecr package. ' +
'It is shipped by default; if you built this image with ' +
'`npm ci --omit=optional`, reinstall without that flag to ' +
'enable ECR.',
{ cause: err },
);
}
const { ECRClient, GetAuthorizationTokenCommand } = sdk;
const client = new ECRClient({
region,
credentials: { accessKeyId, secretAccessKey },