Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
41 changes: 41 additions & 0 deletions examples/typescript/multi-lambda/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { TypeScriptCodeCollection } from '@mrgrain/cdk-esbuild';

export class MultiLambdaStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);

const codeCollection = new TypeScriptCodeCollection(this, 'MultiLambda', {
entryPoints: {
'api': './src/api.ts',
'auth': './src/auth.ts',
'notifications': './src/notifications.ts',
},
buildOptions: {
minify: true,
},
});

new lambda.Function(this, 'ApiFunction', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'api.handler',
code: codeCollection.getCode('api'),
});

new lambda.Function(this, 'AuthFunction', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'auth.handler',
code: codeCollection.getCode('auth'),
});

new lambda.Function(this, 'NotificationsFunction', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'notifications.handler',
Comment on lines +22 to +34
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handler references ('api.handler', 'auth.handler', 'notifications.handler') are incorrect for the current implementation. Since each entry point is bundled separately using individual TypeScriptCode instances, the output will be 'index.js' for each bundle, not 'api.js', 'auth.js', or 'notifications.js'. The handler should be 'index.handler' for all three Lambda functions.

This handler naming would only work if all entry points were bundled together in a single esbuild invocation with a Record<string, string> of entry points, which would preserve the entry point names in the output.

Suggested change
handler: 'api.handler',
code: codeCollection.getCode('api'),
});
new lambda.Function(this, 'AuthFunction', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'auth.handler',
code: codeCollection.getCode('auth'),
});
new lambda.Function(this, 'NotificationsFunction', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'notifications.handler',
handler: 'index.handler',
code: codeCollection.getCode('api'),
});
new lambda.Function(this, 'AuthFunction', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: codeCollection.getCode('auth'),
});
new lambda.Function(this, 'NotificationsFunction', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',

Copilot uses AI. Check for mistakes.
code: codeCollection.getCode('notifications'),
});
}
}

const app = new cdk.App();
new MultiLambdaStack(app, 'MultiLambdaStack');
18 changes: 18 additions & 0 deletions examples/typescript/multi-lambda/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "multi-lambda-example",
"version": "1.0.0",
"description": "Example of building multiple Lambda functions with cdk-esbuild",
"main": "app.ts",
"scripts": {
"deploy": "cdk deploy",
"synth": "cdk synth"
},
"dependencies": {
"aws-cdk-lib": "^2.0.0",
"constructs": "^10.0.0",
"@mrgrain/cdk-esbuild": "^5.0.0"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
Comment on lines +1 to +18
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is missing several files that are present in other TypeScript examples and are necessary for the example to be functional:

  1. cdk.json - Required for CDK CLI to know how to execute the app. Without this, running cdk deploy or cdk synth won't work.
  2. tsconfig.json - Required for TypeScript compilation
  3. README.md - Helpful for users to understand how to use the example
  4. .gitignore - Standard practice for examples

For reference, see the examples/typescript/lambda/ directory which includes all these files.

Copilot uses AI. Check for mistakes.
6 changes: 6 additions & 0 deletions examples/typescript/multi-lambda/src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export async function handler(event: any) {
return {
statusCode: 200,
body: JSON.stringify({ message: 'API Handler' }),
};
}
6 changes: 6 additions & 0 deletions examples/typescript/multi-lambda/src/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export async function handler(event: any) {
return {
statusCode: 200,
body: JSON.stringify({ message: 'Auth Handler' }),
};
}
6 changes: 6 additions & 0 deletions examples/typescript/multi-lambda/src/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export async function handler(event: any) {
return {
statusCode: 200,
body: JSON.stringify({ message: 'Notifications Handler' }),
};
}
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,8 @@ export {
TypeScriptSource,
TypeScriptSourceProps,
} from './source';

export {
TypeScriptCodeCollection,
TypeScriptCodeCollectionProps,
} from './typescript-code-collection';
95 changes: 95 additions & 0 deletions src/typescript-code-collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Construct } from 'constructs';
import { BundlerProps } from './bundler';
import { TypeScriptCode, TypeScriptCodeProps } from './code';

/**
* Properties for TypeScriptCodeCollection
*
* @stability stable
*/
export interface TypeScriptCodeCollectionProps extends BundlerProps {
/**
* Entry points to bundle as a collection.
*
* Key: logical function name (used to retrieve the code later).
* Value: path to the entry point file.
*
* @stability stable
*/
readonly entryPoints: Record<string, string>;

/**
* A hash of this asset, which is available at construction time.
*
* As this is a plain string, it can be used in construct IDs in order to enforce creation of a new resource when the content hash has changed.
*
* Defaults to a hash of all files in the resulting bundle.
*
Comment on lines +24 to +27
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states that assetHash "Defaults to a hash of all files in the resulting bundle," which is misleading. Since each entry point creates a separate TypeScriptCode instance with its own bundle, when assetHash is not provided, each function will have its own independent hash based solely on its own bundle, not a hash of all files across the collection.

The documentation should clarify that: (1) when assetHash is provided, all functions in the collection share the same hash, or (2) when assetHash is not provided, each function gets its own hash based on its individual bundle.

Suggested change
* As this is a plain string, it can be used in construct IDs in order to enforce creation of a new resource when the content hash has changed.
*
* Defaults to a hash of all files in the resulting bundle.
*
* As this is a plain string, it can be used in construct IDs in order to enforce
* creation of a new resource when the content hash has changed.
*
* When `assetHash` is provided on the collection, the same hash value is passed
* to every {@link TypeScriptCode} instance created for the entry points, so all
* functions in the collection share the same asset hash.
*
* When `assetHash` is not provided, each {@link TypeScriptCode} instance computes
* its own default hash based on the files in its individual bundle (for that
* entry point), so functions have independent hashes.
*

Copilot uses AI. Check for mistakes.
* @stability stable
*/
readonly assetHash?: string;
}

/**
* Manages multiple Lambda function entry points that share the same build configuration.
*
* Creates a {@link TypeScriptCode} asset for each entry point, allowing related
* functions to be organized with common build options. This is primarily a
* convenience construct for managing multiple Lambda functions that share
* the same esbuild configuration.
*
Comment on lines +36 to +40
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the class documentation accurately describes this as "primarily a convenience construct for managing multiple Lambda functions that share the same esbuild configuration," this contradicts the PR description which claims "Single esbuild invocation reduces build time" and "Leverages esbuild's native multi-entry point support."

The fundamental issue is that AWS Lambda's Code abstraction expects a single S3 asset location (as seen in TypeScriptCode.bind() returning a single s3Location). Even though TypeScriptCode already supports Record<string, string> for entry points (which would trigger a single esbuild invocation with multiple outputs), there's no way to distribute those multiple outputs to different Lambda functions through the CDK's Lambda construct API.

Therefore, this implementation is likely the only viable approach given CDK's constraints, but the PR description should be updated to reflect that this is a convenience wrapper for organizing related Lambda functions with shared build configuration, NOT a performance optimization through multi-entry point builds.

Suggested change
* Creates a {@link TypeScriptCode} asset for each entry point, allowing related
* functions to be organized with common build options. This is primarily a
* convenience construct for managing multiple Lambda functions that share
* the same esbuild configuration.
*
* Creates a separate {@link TypeScriptCode} asset for each entry point, allowing related
* functions to be organized with common build options. This is primarily a
* convenience construct for managing multiple Lambda functions that share
* the same esbuild configuration and does not provide build-time performance
* optimizations beyond what individual {@link TypeScriptCode} instances offer.
*
* In particular, this construct does *not* perform a single multi-entry esbuild
* invocation that produces multiple outputs; each entry point is bundled
* independently to a distinct asset, in line with the Lambda {@link Code}
* abstraction expecting a single asset location per function.
*

Copilot uses AI. Check for mistakes.
* @stability stable
*/
export class TypeScriptCodeCollection extends Construct {
private readonly codes: { [name: string]: TypeScriptCode } = {};

constructor(scope: Construct, id: string, props: TypeScriptCodeCollectionProps) {
super(scope, id);

const { entryPoints, assetHash, ...bundlerProps } = props;

// Create individual TypeScriptCode instances for each entry point
Object.entries(entryPoints).forEach(([name, entryPoint]) => {
const codeProps: TypeScriptCodeProps = {
...bundlerProps,
assetHash,
};

this.codes[name] = new TypeScriptCode(entryPoint, codeProps);
});
Comment on lines +46 to +59
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description states "Single esbuild invocation reduces build time" as a benefit and claims the construct "allows bundling multiple Lambda functions in a single esbuild invocation." However, the implementation creates separate TypeScriptCode instances for each function, which will result in N separate esbuild invocations (one per function), not a single invocation. This is a fundamental discrepancy between what the PR claims to deliver and what it actually implements.

Copilot uses AI. Check for mistakes.
}

/**
* Get the bundled TypeScript code for a specific function.
*
* @param functionName The logical function name (key from entryPoints)
* @returns TypeScriptCode instance that can be used with Lambda.Function
*
* @stability stable
*/
public getCode(functionName: string): TypeScriptCode {
const code = this.codes[functionName];
if (!code) {
throw new Error(`No entry point found for function: ${functionName}`);
}
return code;
}

/**
* The names of all functions in this collection.
*
* @stability stable
*/
public get functionNames(): string[] {
return Object.keys(this.codes);
}

/**
* All bundled code instances.
*
* @stability stable
*/
public get allCodes(): { [name: string]: TypeScriptCode } {
return { ...this.codes };
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no tests for the new TypeScriptCodeCollection class. Given that this codebase has comprehensive test coverage for other constructs (as seen in test/code.test.ts, test/source.test.ts, etc.), tests should be added to verify the functionality. Tests should cover:

  • Correct instantiation with multiple entry points
  • getCode() method returns correct code for each function
  • Error handling when requesting non-existent function names
  • Build behavior with shared configuration
Suggested change
}
}
/**
* Get the names of all functions in this collection.
*
* This is a convenience method to support introspection and testing
* without exposing the internal codeAssets map.
*/
public getFunctionNames(): string[] {
return Array.from(this.codeAssets.keys());
}

Copilot uses AI. Check for mistakes.
}
74 changes: 74 additions & 0 deletions test/integ/integ-multi-lambda.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import aws_cdk as cdk
import aws_cdk.aws_lambda as lambda_
import aws_cdk.integ_tests_alpha as integ
from constructs import Construct
from mrgrain.cdk_esbuild import (
BuildOptions,
TypeScriptCodeCollection,
EsbuildProvider,
EsbuildSource,
)


class MultiLambdaStack(cdk.Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)

# Set a new default
EsbuildProvider.override_default_provider(
EsbuildProvider(esbuild_module_path=EsbuildSource.install())
)

# Use TypeScriptCodeCollection to build multiple functions
code_collection = TypeScriptCodeCollection(
self,
"MultiLambda",
entry_points={
"ts_handler": "test/fixtures/handlers/ts-handler.ts",
"js_handler": "test/fixtures/handlers/js-handler.js",
},
)

lambda_.Function(
self,
"TsHandlerFunction",
runtime=lambda_.Runtime.NODEJS_18_X,
handler="index.handler",
code=code_collection.get_code("ts_handler"),
)

lambda_.Function(
self,
"JsHandlerFunction",
runtime=lambda_.Runtime.NODEJS_18_X,
handler="index.handler",
code=code_collection.get_code("js_handler"),
)

# Use TypeScriptCodeCollection with build options
code_collection_with_options = TypeScriptCodeCollection(
self,
"MultiLambdaWithOptions",
entry_points={
"colors": "test/fixtures/handlers/colors.ts",
},
build_options=BuildOptions(
external=["aws-sdk"],
),
)

lambda_.Function(
self,
"ColorsFunction",
runtime=lambda_.Runtime.NODEJS_18_X,
handler="index.handler",
code=code_collection_with_options.get_code("colors"),
)


app = cdk.App()
stack = MultiLambdaStack(app, "MultiLambda")

integ.IntegTest(app, "MultiLambdaFunctions", test_cases=[stack])

app.synth()
Loading
Loading