-
-
Notifications
You must be signed in to change notification settings - Fork 10
feat: Add TypeScriptCodeCollection for building multiple Lambda functions #1701
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v5
Are you sure you want to change the base?
Changes from 4 commits
38248db
f95c50a
2f659fb
f7bb156
59e5f21
1d8b1e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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', | ||
| code: codeCollection.getCode('notifications'), | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| const app = new cdk.App(); | ||
| new MultiLambdaStack(app, 'MultiLambdaStack'); | ||
| 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
|
||
| 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' }), | ||
| }; | ||
| } |
| 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' }), | ||
| }; | ||
| } |
| 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' }), | ||
| }; | ||
| } |
| 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
|
||||||||||||||||||||||||||||||||||
| * 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
AI
Feb 17, 2026
There was a problem hiding this comment.
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.
| * 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
AI
Feb 14, 2026
There was a problem hiding this comment.
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
AI
Feb 14, 2026
There was a problem hiding this comment.
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
| } | |
| } | |
| /** | |
| * 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()); | |
| } |
| 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() |
There was a problem hiding this comment.
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.