diff --git a/API.md b/API.md index 582e9369..23ba197b 100644 --- a/API.md +++ b/API.md @@ -288,6 +288,155 @@ For example, `s3://bucket/key` --- +### TypeScriptCodeCollection + +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. + +#### Initializers + +```typescript +import { TypeScriptCodeCollection } from '@mrgrain/cdk-esbuild' + +new TypeScriptCodeCollection(scope: Construct, id: string, props: TypeScriptCodeCollectionProps) +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| scope | constructs.Construct | *No description.* | +| id | string | *No description.* | +| props | TypeScriptCodeCollectionProps | *No description.* | + +--- + +##### `scope`Required + +- *Type:* constructs.Construct + +--- + +##### `id`Required + +- *Type:* string + +--- + +##### `props`Required + +- *Type:* TypeScriptCodeCollectionProps + +--- + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| toString | Returns a string representation of this construct. | +| getCode | Get the bundled TypeScript code for a specific function. | + +--- + +##### `toString` + +```typescript +public toString(): string +``` + +Returns a string representation of this construct. + +##### `getCode` + +```typescript +public getCode(functionName: string): TypeScriptCode +``` + +Get the bundled TypeScript code for a specific function. + +###### `functionName`Required + +- *Type:* string + +The logical function name (key from entryPoints). + +--- + +#### Static Functions + +| **Name** | **Description** | +| --- | --- | +| isConstruct | Checks if `x` is a construct. | + +--- + +##### ~~`isConstruct`~~ + +```typescript +import { TypeScriptCodeCollection } from '@mrgrain/cdk-esbuild' + +TypeScriptCodeCollection.isConstruct(x: any) +``` + +Checks if `x` is a construct. + +###### `x`Required + +- *Type:* any + +Any object. + +--- + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| node | constructs.Node | The tree node. | +| allCodes | {[ key: string ]: TypeScriptCode} | All bundled code instances. | +| functionNames | string[] | The names of all functions in this collection. | + +--- + +##### `node`Required + +```typescript +public readonly node: Node; +``` + +- *Type:* constructs.Node + +The tree node. + +--- + +##### `allCodes`Required + +```typescript +public readonly allCodes: {[ key: string ]: TypeScriptCode}; +``` + +- *Type:* {[ key: string ]: TypeScriptCode} + +All bundled code instances. + +--- + +##### `functionNames`Required + +```typescript +public readonly functionNames: string[]; +``` + +- *Type:* string[] + +The names of all functions in this collection. + +--- + + ## Structs ### BuildOptions @@ -3888,6 +4037,130 @@ Examples: --- +### TypeScriptCodeCollectionProps + +Properties for TypeScriptCodeCollection. + +#### Initializer + +```typescript +import { TypeScriptCodeCollectionProps } from '@mrgrain/cdk-esbuild' + +const typeScriptCodeCollectionProps: TypeScriptCodeCollectionProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| buildOptions | BuildOptions | Build options passed on to esbuild. Please refer to the esbuild Build API docs for details. | +| buildProvider | IBuildProvider | The esbuild Build API implementation to be used. | +| copyDir | string \| string[] \| {[ key: string ]: string \| string[]} | Copy additional files to the code [asset staging directory](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.AssetStaging.html#absolutestagedpath), before the build runs. Files copied like this will be overwritten by esbuild if they share the same name as any of the outputs. | +| entryPoints | {[ key: string ]: string} | Entry points to bundle as a collection. | +| assetHash | string | A hash of this asset, which is available at construction time. | + +--- + +##### `buildOptions`Optional + +```typescript +public readonly buildOptions: BuildOptions; +``` + +- *Type:* BuildOptions + +Build options passed on to esbuild. Please refer to the esbuild Build API docs for details. + +* `buildOptions.outdir: string` +The actual path for the output directory is defined by CDK. However setting this option allows to write files into a subdirectory. \ +For example `{ outdir: 'js' }` will create an asset with a single directory called `js`, which contains all built files. This approach can be useful for static website deployments, where JavaScript code should be placed into a subdirectory. \ +*Cannot be used together with `outfile`*. +* `buildOptions.outfile: string` +Relative path to a file inside the CDK asset output directory. +For example `{ outfile: 'js/index.js' }` will create an asset with a single directory called `js`, which contains a single file `index.js`. This can be useful to rename the entry point. \ +*Cannot be used with multiple entryPoints or together with `outdir`.* +* `buildOptions.absWorkingDir: string` +Absolute path to the [esbuild working directory](https://esbuild.github.io/api/#working-directory) and defaults to the [current working directory](https://en.wikipedia.org/wiki/Working_directory). \ +If paths cannot be found, a good starting point is to look at the concatenation of `absWorkingDir + entryPoint`. It must always be a valid absolute path pointing to the entry point. When needed, the probably easiest way to set absWorkingDir is to use a combination of `resolve` and `__dirname` (see "Library authors" section in the documentation). + +> [https://esbuild.github.io/api/#build-api](https://esbuild.github.io/api/#build-api) + +--- + +##### `buildProvider`Optional + +```typescript +public readonly buildProvider: IBuildProvider; +``` + +- *Type:* IBuildProvider +- *Default:* new EsbuildProvider() + +The esbuild Build API implementation to be used. + +Configure the default `EsbuildProvider` for more options or +provide a custom `IBuildProvider` as an escape hatch. + +--- + +##### `copyDir`Optional + +```typescript +public readonly copyDir: string | string[] | {[ key: string ]: string | string[]}; +``` + +- *Type:* string | string[] | {[ key: string ]: string | string[]} + +Copy additional files to the code [asset staging directory](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.AssetStaging.html#absolutestagedpath), before the build runs. Files copied like this will be overwritten by esbuild if they share the same name as any of the outputs. + +* When provided with a `string` or `array`, all files are copied to the root of asset staging directory. +* When given a `map`, the key indicates the destination relative to the asset staging directory and the value is a list of all sources to be copied. + +Therefore the following values for `copyDir` are all equivalent: +``` +{ copyDir: "path/to/source" } +{ copyDir: ["path/to/source"] } +{ copyDir: { ".": "path/to/source" } } +{ copyDir: { ".": ["path/to/source"] } } +``` +The destination cannot be outside of the asset staging directory. +If you are receiving the error "Cannot copy files to outside of the asset staging directory." +you are likely using `..` or an absolute path as key on the `copyDir` map. +Instead use only relative paths and avoid `..`. + +--- + +##### `entryPoints`Required + +```typescript +public readonly entryPoints: {[ key: string ]: string}; +``` + +- *Type:* {[ key: string ]: string} + +Entry points to bundle as a collection. + +Key: logical function name (used to retrieve the code later). +Value: path to the entry point file. + +--- + +##### `assetHash`Optional + +```typescript +public readonly assetHash: string; +``` + +- *Type:* 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. + +--- + ### TypeScriptCodeProps #### Initializer diff --git a/examples/typescript/multi-lambda/app.ts b/examples/typescript/multi-lambda/app.ts new file mode 100644 index 00000000..9726da7e --- /dev/null +++ b/examples/typescript/multi-lambda/app.ts @@ -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'); \ No newline at end of file diff --git a/examples/typescript/multi-lambda/package.json b/examples/typescript/multi-lambda/package.json new file mode 100644 index 00000000..bd3423bf --- /dev/null +++ b/examples/typescript/multi-lambda/package.json @@ -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" + } +} \ No newline at end of file diff --git a/examples/typescript/multi-lambda/src/api.ts b/examples/typescript/multi-lambda/src/api.ts new file mode 100644 index 00000000..4e20b110 --- /dev/null +++ b/examples/typescript/multi-lambda/src/api.ts @@ -0,0 +1,6 @@ +export async function handler(event: any) { + return { + statusCode: 200, + body: JSON.stringify({ message: 'API Handler' }), + }; +} \ No newline at end of file diff --git a/examples/typescript/multi-lambda/src/auth.ts b/examples/typescript/multi-lambda/src/auth.ts new file mode 100644 index 00000000..8b6a7211 --- /dev/null +++ b/examples/typescript/multi-lambda/src/auth.ts @@ -0,0 +1,6 @@ +export async function handler(event: any) { + return { + statusCode: 200, + body: JSON.stringify({ message: 'Auth Handler' }), + }; +} \ No newline at end of file diff --git a/examples/typescript/multi-lambda/src/notifications.ts b/examples/typescript/multi-lambda/src/notifications.ts new file mode 100644 index 00000000..4d84aec3 --- /dev/null +++ b/examples/typescript/multi-lambda/src/notifications.ts @@ -0,0 +1,6 @@ +export async function handler(event: any) { + return { + statusCode: 200, + body: JSON.stringify({ message: 'Notifications Handler' }), + }; +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 2d80477e..912f88ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,3 +49,8 @@ export { TypeScriptSource, TypeScriptSourceProps, } from './source'; + +export { + TypeScriptCodeCollection, + TypeScriptCodeCollectionProps, +} from './typescript-code-collection'; diff --git a/src/typescript-code-collection.ts b/src/typescript-code-collection.ts new file mode 100644 index 00000000..1c591dfe --- /dev/null +++ b/src/typescript-code-collection.ts @@ -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; + + /** + * 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. + * + * @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. + * + * @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); + }); + } + + /** + * 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 }; + } +} diff --git a/test/integ/integ-multi-lambda.py b/test/integ/integ-multi-lambda.py new file mode 100644 index 00000000..6938be65 --- /dev/null +++ b/test/integ/integ-multi-lambda.py @@ -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() diff --git a/test/typescript-code-collection.test.ts b/test/typescript-code-collection.test.ts new file mode 100644 index 00000000..208820ea --- /dev/null +++ b/test/typescript-code-collection.test.ts @@ -0,0 +1,302 @@ +import { resolve } from 'path'; +import { Stack } from 'aws-cdk-lib'; +import { Function, Runtime as LambdaRuntime } from 'aws-cdk-lib/aws-lambda'; +import { EsbuildProvider } from '../src/provider'; +import { TypeScriptCodeCollection } from '../src/typescript-code-collection'; + +const buildProvider = new EsbuildProvider(); +const buildSyncSpy = jest.spyOn(buildProvider, 'buildSync'); + +describe('TypeScriptCodeCollection', () => { + describe('basic instantiation', () => { + it('should create a collection with multiple entry points', () => { + expect(() => { + const stack = new Stack(); + + const collection = new TypeScriptCodeCollection(stack, 'Collection', { + entryPoints: { + handler1: 'fixtures/handlers/ts-handler.ts', + handler2: 'fixtures/handlers/js-handler.js', + }, + buildOptions: { absWorkingDir: resolve(__dirname) }, + }); + + new Function(stack, 'Function1', { + runtime: LambdaRuntime.NODEJS_18_X, + handler: 'index.handler', + code: collection.getCode('handler1'), + }); + + new Function(stack, 'Function2', { + runtime: LambdaRuntime.NODEJS_18_X, + handler: 'index.handler', + code: collection.getCode('handler2'), + }); + }).not.toThrow(); + }); + + it('should create a collection with a single entry point', () => { + expect(() => { + const stack = new Stack(); + + const collection = new TypeScriptCodeCollection(stack, 'Collection', { + entryPoints: { + handler: 'fixtures/handlers/ts-handler.ts', + }, + buildOptions: { absWorkingDir: resolve(__dirname) }, + }); + + new Function(stack, 'Function1', { + runtime: LambdaRuntime.NODEJS_18_X, + handler: 'index.handler', + code: collection.getCode('handler'), + }); + }).not.toThrow(); + }); + }); + + describe('getCode()', () => { + it('should return TypeScriptCode for a valid function name', () => { + const stack = new Stack(); + + const collection = new TypeScriptCodeCollection(stack, 'Collection', { + entryPoints: { + handler1: 'fixtures/handlers/ts-handler.ts', + handler2: 'fixtures/handlers/js-handler.js', + }, + buildOptions: { absWorkingDir: resolve(__dirname) }, + }); + + const code = collection.getCode('handler1'); + expect(code).toBeDefined(); + }); + + it('should throw an error for non-existent function name', () => { + const stack = new Stack(); + + const collection = new TypeScriptCodeCollection(stack, 'Collection', { + entryPoints: { + handler1: 'fixtures/handlers/ts-handler.ts', + }, + buildOptions: { absWorkingDir: resolve(__dirname) }, + }); + + expect(() => collection.getCode('nonExistent')).toThrow( + 'No entry point found for function: nonExistent', + ); + }); + }); + + describe('functionNames', () => { + it('should return all function names', () => { + const stack = new Stack(); + + const collection = new TypeScriptCodeCollection(stack, 'Collection', { + entryPoints: { + api: 'fixtures/handlers/ts-handler.ts', + auth: 'fixtures/handlers/js-handler.js', + }, + buildOptions: { absWorkingDir: resolve(__dirname) }, + }); + + const names = collection.functionNames; + expect(names).toHaveLength(2); + expect(names).toContain('api'); + expect(names).toContain('auth'); + }); + + it('should return empty array when no entry points', () => { + const stack = new Stack(); + + const collection = new TypeScriptCodeCollection(stack, 'Collection', { + entryPoints: {}, + buildOptions: { absWorkingDir: resolve(__dirname) }, + }); + + expect(collection.functionNames).toHaveLength(0); + }); + }); + + describe('allCodes', () => { + it('should return all code instances', () => { + const stack = new Stack(); + + const collection = new TypeScriptCodeCollection(stack, 'Collection', { + entryPoints: { + api: 'fixtures/handlers/ts-handler.ts', + auth: 'fixtures/handlers/js-handler.js', + }, + buildOptions: { absWorkingDir: resolve(__dirname) }, + }); + + const allCodes = collection.allCodes; + expect(Object.keys(allCodes)).toHaveLength(2); + expect(allCodes.api).toBeDefined(); + expect(allCodes.auth).toBeDefined(); + }); + + it('should return a copy (not a reference to internal state)', () => { + const stack = new Stack(); + + const collection = new TypeScriptCodeCollection(stack, 'Collection', { + entryPoints: { + api: 'fixtures/handlers/ts-handler.ts', + }, + buildOptions: { absWorkingDir: resolve(__dirname) }, + }); + + const allCodes = collection.allCodes; + // Modifying the returned object should not affect internal state + delete allCodes.api; + expect(collection.getCode('api')).toBeDefined(); + }); + }); + + describe('shared build configuration', () => { + it('should pass build options to all code instances', () => { + expect(() => { + const stack = new Stack(); + + const collection = new TypeScriptCodeCollection(stack, 'Collection', { + entryPoints: { + handler1: 'fixtures/handlers/ts-handler.ts', + handler2: 'fixtures/handlers/js-handler.js', + }, + buildOptions: { + absWorkingDir: resolve(__dirname), + minify: true, + sourcemap: true, + }, + buildProvider, + }); + + new Function(stack, 'Function1', { + runtime: LambdaRuntime.NODEJS_18_X, + handler: 'index.handler', + code: collection.getCode('handler1'), + }); + + new Function(stack, 'Function2', { + runtime: LambdaRuntime.NODEJS_18_X, + handler: 'index.handler', + code: collection.getCode('handler2'), + }); + }).not.toThrow(); + + expect(buildSyncSpy).toHaveBeenCalledWith( + expect.objectContaining({ + entryPoints: ['fixtures/handlers/ts-handler.ts'], + minify: true, + sourcemap: true, + }), + ); + + expect(buildSyncSpy).toHaveBeenCalledWith( + expect.objectContaining({ + entryPoints: ['fixtures/handlers/js-handler.js'], + minify: true, + sourcemap: true, + }), + ); + }); + }); + + describe('custom build provider', () => { + it('should use the provided build provider for all entry points', () => { + expect(() => { + const stack = new Stack(); + + const collection = new TypeScriptCodeCollection(stack, 'Collection', { + entryPoints: { + handler1: 'fixtures/handlers/ts-handler.ts', + }, + buildOptions: { absWorkingDir: resolve(__dirname) }, + buildProvider, + }); + + new Function(stack, 'Function1', { + runtime: LambdaRuntime.NODEJS_18_X, + handler: 'index.handler', + code: collection.getCode('handler1'), + }); + }).not.toThrow(); + + expect(buildSyncSpy).toHaveBeenCalledWith( + expect.objectContaining({ + entryPoints: ['fixtures/handlers/ts-handler.ts'], + }), + ); + }); + }); + + describe('using a custom asset hash', () => { + it('should not throw', () => { + expect(() => { + const stack = new Stack(); + + const collection = new TypeScriptCodeCollection(stack, 'Collection', { + entryPoints: { + handler1: 'fixtures/handlers/ts-handler.ts', + }, + buildOptions: { absWorkingDir: resolve(__dirname) }, + assetHash: 'customhash1234567890', + }); + + new Function(stack, 'Function1', { + runtime: LambdaRuntime.NODEJS_18_X, + handler: 'index.handler', + code: collection.getCode('handler1'), + }); + }).not.toThrow(); + }); + }); + + describe('Lambda functions with collection', () => { + it('should create separate Lambda functions from the same collection', () => { + expect(() => { + const stack = new Stack(); + + const collection = new TypeScriptCodeCollection(stack, 'Collection', { + entryPoints: { + api: 'fixtures/handlers/ts-handler.ts', + auth: 'fixtures/handlers/js-handler.js', + }, + buildOptions: { absWorkingDir: resolve(__dirname) }, + }); + + new Function(stack, 'ApiFunction', { + runtime: LambdaRuntime.NODEJS_18_X, + handler: 'api.handler', + code: collection.getCode('api'), + }); + + new Function(stack, 'AuthFunction', { + runtime: LambdaRuntime.NODEJS_18_X, + handler: 'auth.handler', + code: collection.getCode('auth'), + }); + }).not.toThrow(); + }); + }); + + describe('error handling', () => { + it('should report build failures for invalid entry points', () => { + expect(() => { + const stack = new Stack(); + + const collection = new TypeScriptCodeCollection(stack, 'Collection', { + entryPoints: { + invalid: 'fixtures/handlers/invalid-handler.js', + }, + buildOptions: { absWorkingDir: resolve(__dirname) }, + }); + + new Function(stack, 'Function1', { + runtime: LambdaRuntime.NODEJS_18_X, + handler: 'index.handler', + code: collection.getCode('invalid'), + }); + }).toThrow('Esbuild failed to bundle fixtures/handlers/invalid-handler.js'); + }); + }); +});