From 81614caffbd06123a146404ebd3ae5b724e0e250 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Sat, 12 Mar 2022 17:10:16 -0700 Subject: [PATCH] feat(awscdk): add AWS Lambda Extension development support (#1647) Adds AWS Lambda Extension development support to AWS CDK projects. Fixes #1646 --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --- docs/api/API.md | 136 ++++++++++- docs/awscdk.md | 136 +++++++++++ src/awscdk/auto-discover.ts | 65 ++++- src/awscdk/awscdk-app-ts.ts | 10 + src/awscdk/awscdk-construct.ts | 10 + src/awscdk/index.ts | 1 + src/awscdk/internal.ts | 14 ++ src/awscdk/lambda-extension.ts | 229 ++++++++++++++++++ src/awscdk/lambda-function.ts | 11 +- src/javascript/bundler.ts | 25 +- test/__snapshots__/inventory.test.ts.snap | 34 +++ .../lambda-extension.test.ts.snap | 32 +++ test/awscdk/lambda-extension.test.ts | 199 +++++++++++++++ 13 files changed, 885 insertions(+), 17 deletions(-) create mode 100644 src/awscdk/lambda-extension.ts create mode 100644 test/awscdk/__snapshots__/lambda-extension.test.ts.snap create mode 100644 test/awscdk/lambda-extension.test.ts diff --git a/docs/api/API.md b/docs/api/API.md index 99f64c1b1f7..f6e51fc429d 100644 --- a/docs/api/API.md +++ b/docs/api/API.md @@ -51,6 +51,8 @@ Name|Description [awscdk.IntegrationTest](#projen-awscdk-integrationtest)|Cloud integration tests. [awscdk.IntegrationTestAutoDiscover](#projen-awscdk-integrationtestautodiscover)|Creates integration tests from entry points discovered in the test tree. [awscdk.LambdaAutoDiscover](#projen-awscdk-lambdaautodiscover)|Creates lambdas from entry points discovered in the project's source tree. +[awscdk.LambdaExtension](#projen-awscdk-lambdaextension)|Create a Lambda Extension. +[awscdk.LambdaExtensionAutoDiscover](#projen-awscdk-lambdaextensionautodiscover)|Creates Lambda Extensions from entrypoints discovered in the project's source tree. [awscdk.LambdaFunction](#projen-awscdk-lambdafunction)|Generates a pre-bundled AWS Lambda function construct from handler code. [awscdk.LambdaRuntime](#projen-awscdk-lambdaruntime)|The runtime for the AWS Lambda function. [build.BuildWorkflow](#projen-build-buildworkflow)|*No description* @@ -195,6 +197,9 @@ Name|Description [awscdk.IntegrationTestCommonOptions](#projen-awscdk-integrationtestcommonoptions)|*No description* [awscdk.IntegrationTestOptions](#projen-awscdk-integrationtestoptions)|Options for `IntegrationTest`. [awscdk.LambdaAutoDiscoverOptions](#projen-awscdk-lambdaautodiscoveroptions)|Options for `LambdaAutoDiscover`. +[awscdk.LambdaExtensionAutoDiscoverOptions](#projen-awscdk-lambdaextensionautodiscoveroptions)|Options for `LambdaExtensionAutoDiscover`. +[awscdk.LambdaExtensionCommonOptions](#projen-awscdk-lambdaextensioncommonoptions)|Common options for creating lambda extensions. +[awscdk.LambdaExtensionOptions](#projen-awscdk-lambdaextensionoptions)|Options for creating lambda extensions. [awscdk.LambdaFunctionCommonOptions](#projen-awscdk-lambdafunctioncommonoptions)|Common options for `LambdaFunction`. [awscdk.LambdaFunctionOptions](#projen-awscdk-lambdafunctionoptions)|Options for `Function`. [build.AddPostBuildJobCommandsOptions](#projen-build-addpostbuildjobcommandsoptions)|Options for `BuildWorkflow.addPostBuildJobCommands`. @@ -2976,11 +2981,13 @@ new awscdk.AutoDiscover(project: Project, options: AutoDiscoverOptions) * **cdkDeps** ([awscdk.AwsCdkDeps](#projen-awscdk-awscdkdeps)) AWS CDK dependency manager. * **tsconfigPath** (string) Path to the tsconfig file to use for integration tests. * **srcdir** (string) Project source tree (relative to project output directory). - * **lambdaOptions** ([awscdk.LambdaFunctionCommonOptions](#projen-awscdk-lambdafunctioncommonoptions)) Options for auto-discovery of AWS Lambda functions. __*Optional*__ + * **lambdaOptions** ([awscdk.LambdaFunctionCommonOptions](#projen-awscdk-lambdafunctioncommonoptions)) Options for AWS Lambda functions. __*Optional*__ + * **lambdaExtensionOptions** ([awscdk.LambdaExtensionCommonOptions](#projen-awscdk-lambdaextensioncommonoptions)) Options for lambda extensions. __*Optional*__ * **testdir** (string) Test source tree. * **integrationTestOptions** ([awscdk.IntegrationTestCommonOptions](#projen-awscdk-integrationtestcommonoptions)) Options for integration tests. __*Optional*__ * **integrationTestAutoDiscover** (boolean) Auto-discover integration tests. __*Default*__: true * **lambdaAutoDiscover** (boolean) Auto-discover lambda functions. __*Default*__: true + * **lambdaExtensionAutoDiscover** (boolean) Auto-discover lambda extensions. __*Default*__: true @@ -3161,6 +3168,7 @@ new awscdk.AwsCdkConstructLibrary(options: AwsCdkConstructLibraryOptions) * **constructsVersion** (string) Minimum version of the `constructs` library to depend on. __*Default*__: for CDK 1.x the default is "3.2.27", for CDK 2.x the default is "10.0.5". * **integrationTestAutoDiscover** (boolean) Automatically discovers and creates integration tests for each `.integ.ts` file in under your test directory. __*Default*__: true * **lambdaAutoDiscover** (boolean) Automatically adds an `aws_lambda.Function` for each `.lambda.ts` handler in your source tree. If this is disabled, you either need to explicitly call `aws_lambda.Function.autoDiscover()` or define a `new aws_lambda.Function()` for each handler. __*Default*__: true + * **lambdaExtensionAutoDiscover** (boolean) Automatically adds an `awscdk.LambdaExtension` for each `.lambda-extension.ts` entrypoint in your source tree. If this is disabled, you can manually add an `awscdk.AutoDiscover` component to your project. __*Default*__: true * **lambdaOptions** ([awscdk.LambdaFunctionCommonOptions](#projen-awscdk-lambdafunctioncommonoptions)) Common options for all AWS Lambda functions. __*Default*__: default options @@ -3761,6 +3769,7 @@ new awscdk.AwsCdkTypeScriptApp(options: AwsCdkTypeScriptAppOptions) * **appEntrypoint** (string) The CDK app's entrypoint (relative to the source directory, which is "src" by default). __*Default*__: "main.ts" * **integrationTestAutoDiscover** (boolean) Automatically discovers and creates integration tests for each `.integ.ts` file in under your test directory. __*Default*__: true * **lambdaAutoDiscover** (boolean) Automatically adds an `awscdk.LambdaFunction` for each `.lambda.ts` handler in your source tree. If this is disabled, you can manually add an `awscdk.AutoDiscover` component to your project. __*Default*__: true + * **lambdaExtensionAutoDiscover** (boolean) Automatically adds an `awscdk.LambdaExtension` for each `.lambda-extension.ts` entrypoint in your source tree. If this is disabled, you can manually add an `awscdk.AutoDiscover` component to your project. __*Default*__: true * **lambdaOptions** ([awscdk.LambdaFunctionCommonOptions](#projen-awscdk-lambdafunctioncommonoptions)) Common options for all AWS Lambda functions. __*Default*__: default options @@ -4041,6 +4050,7 @@ new awscdk.ConstructLibraryAws(options: AwsCdkConstructLibraryOptions) * **constructsVersion** (string) Minimum version of the `constructs` library to depend on. __*Default*__: for CDK 1.x the default is "3.2.27", for CDK 2.x the default is "10.0.5". * **integrationTestAutoDiscover** (boolean) Automatically discovers and creates integration tests for each `.integ.ts` file in under your test directory. __*Default*__: true * **lambdaAutoDiscover** (boolean) Automatically adds an `aws_lambda.Function` for each `.lambda.ts` handler in your source tree. If this is disabled, you either need to explicitly call `aws_lambda.Function.autoDiscover()` or define a `new aws_lambda.Function()` for each handler. __*Default*__: true + * **lambdaExtensionAutoDiscover** (boolean) Automatically adds an `awscdk.LambdaExtension` for each `.lambda-extension.ts` entrypoint in your source tree. If this is disabled, you can manually add an `awscdk.AutoDiscover` component to your project. __*Default*__: true * **lambdaOptions** ([awscdk.LambdaFunctionCommonOptions](#projen-awscdk-lambdafunctioncommonoptions)) Common options for all AWS Lambda functions. __*Default*__: default options @@ -4134,7 +4144,64 @@ new awscdk.LambdaAutoDiscover(project: Project, options: LambdaAutoDiscoverOptio * **cdkDeps** ([awscdk.AwsCdkDeps](#projen-awscdk-awscdkdeps)) AWS CDK dependency manager. * **tsconfigPath** (string) Path to the tsconfig file to use for integration tests. * **srcdir** (string) Project source tree (relative to project output directory). - * **lambdaOptions** ([awscdk.LambdaFunctionCommonOptions](#projen-awscdk-lambdafunctioncommonoptions)) Options for auto-discovery of AWS Lambda functions. __*Optional*__ + * **lambdaOptions** ([awscdk.LambdaFunctionCommonOptions](#projen-awscdk-lambdafunctioncommonoptions)) Options for AWS Lambda functions. __*Optional*__ + + + + +## class LambdaExtension 🔹 + +Create a Lambda Extension. + +__Submodule__: awscdk + +__Extends__: [Component](#projen-component) + +### Initializer + + + + +```ts +new awscdk.LambdaExtension(project: Project, options: LambdaExtensionOptions) +``` + +* **project** ([Project](#projen-project)) *No description* +* **options** ([awscdk.LambdaExtensionOptions](#projen-awscdk-lambdaextensionoptions)) *No description* + * **bundlingOptions** ([javascript.BundlingOptions](#projen-javascript-bundlingoptions)) Bundling options for this AWS Lambda extension. __*Default*__: defaults + * **compatibleRuntimes** (Array<[awscdk.LambdaRuntime](#projen-awscdk-lambdaruntime)>) The extension's compatible runtimes. __*Optional*__ + * **cdkDeps** ([awscdk.AwsCdkDeps](#projen-awscdk-awscdkdeps)) AWS CDK dependency manager. + * **entrypoint** (string) A path from the project root directory to a TypeScript file which contains the AWS Lambda extension entrypoint (stand-alone script). + * **constructFile** (string) The name of the generated TypeScript source file. __*Default*__: The name of the entrypoint file, with the `-layer-version.ts` suffix instead of `.lambda-extension.ts`. + * **constructName** (string) The name of the generated `lambda.LayerVersion` subclass. __*Default*__: A pascal cased version of the name of the entrypoint file, with the extension `LayerVersion` (e.g. `AppConfigLayerVersion`). + * **name** (string) Name of the extension. __*Default*__: Derived from the entrypoint filename. + + + + +## class LambdaExtensionAutoDiscover 🔹 + +Creates Lambda Extensions from entrypoints discovered in the project's source tree. + +__Submodule__: awscdk + +__Extends__: [cdk.AutoDiscoverBase](#projen-cdk-autodiscoverbase) + +### Initializer + + + + +```ts +new awscdk.LambdaExtensionAutoDiscover(project: Project, options: LambdaExtensionAutoDiscoverOptions) +``` + +* **project** ([Project](#projen-project)) *No description* +* **options** ([awscdk.LambdaExtensionAutoDiscoverOptions](#projen-awscdk-lambdaextensionautodiscoveroptions)) *No description* + * **cdkDeps** ([awscdk.AwsCdkDeps](#projen-awscdk-awscdkdeps)) AWS CDK dependency manager. + * **tsconfigPath** (string) Path to the tsconfig file to use for integration tests. + * **srcdir** (string) Project source tree (relative to project output directory). + * **lambdaExtensionOptions** ([awscdk.LambdaExtensionCommonOptions](#projen-awscdk-lambdaextensioncommonoptions)) Options for lambda extensions. __*Optional*__ @@ -4355,7 +4422,7 @@ Base class for auto-discovering and creating project subcomponents. __Submodule__: cdk __Extends__: [Component](#projen-component) -__Implemented by__: [awscdk.IntegrationTestAutoDiscover](#projen-awscdk-integrationtestautodiscover), [awscdk.LambdaAutoDiscover](#projen-awscdk-lambdaautodiscover), [cdk.IntegrationTestAutoDiscoverBase](#projen-cdk-integrationtestautodiscoverbase), [cdk8s.IntegrationTestAutoDiscover](#projen-cdk8s-integrationtestautodiscover) +__Implemented by__: [awscdk.IntegrationTestAutoDiscover](#projen-awscdk-integrationtestautodiscover), [awscdk.LambdaAutoDiscover](#projen-awscdk-lambdaautodiscover), [awscdk.LambdaExtensionAutoDiscover](#projen-awscdk-lambdaextensionautodiscover), [cdk.IntegrationTestAutoDiscoverBase](#projen-cdk-integrationtestautodiscoverbase), [cdk8s.IntegrationTestAutoDiscover](#projen-cdk8s-integrationtestautodiscover) ### Initializer @@ -6748,6 +6815,8 @@ addBundle(entrypoint: string, options: AddBundleOptions): Bundle * **watchTask** (boolean) In addition to the `bundle:xyz` task, creates `bundle:xyz:watch` task which will invoke the same esbuild command with the `--watch` flag. __*Default*__: true * **platform** (string) esbuild platform. * **target** (string) esbuild target. + * **executable** (boolean) Mark the output file as executable. __*Default*__: false + * **outfile** (string) Bundler output path relative to the asset's output directory. __*Default*__: "index.js" __Returns__: * [javascript.Bundle](#projen-javascript-bundle) @@ -11485,7 +11554,9 @@ Name | Type | Description **integrationTestAutoDiscover**?🔹 | boolean | Auto-discover integration tests.
__*Default*__: true **integrationTestOptions**?🔹 | [awscdk.IntegrationTestCommonOptions](#projen-awscdk-integrationtestcommonoptions) | Options for integration tests.
__*Optional*__ **lambdaAutoDiscover**?🔹 | boolean | Auto-discover lambda functions.
__*Default*__: true -**lambdaOptions**?🔹 | [awscdk.LambdaFunctionCommonOptions](#projen-awscdk-lambdafunctioncommonoptions) | Options for auto-discovery of AWS Lambda functions.
__*Optional*__ +**lambdaExtensionAutoDiscover**?🔹 | boolean | Auto-discover lambda extensions.
__*Default*__: true +**lambdaExtensionOptions**?🔹 | [awscdk.LambdaExtensionCommonOptions](#projen-awscdk-lambdaextensioncommonoptions) | Options for lambda extensions.
__*Optional*__ +**lambdaOptions**?🔹 | [awscdk.LambdaFunctionCommonOptions](#projen-awscdk-lambdafunctioncommonoptions) | Options for AWS Lambda functions.
__*Optional*__ @@ -11567,6 +11638,7 @@ Name | Type | Description **jsiiReleaseVersion**?🔹 | string | Version requirement of `publib` which is used to publish modules to npm.
__*Default*__: "latest" **keywords**?🔹 | Array | Keywords to include in `package.json`.
__*Optional*__ **lambdaAutoDiscover**?🔹 | boolean | Automatically adds an `aws_lambda.Function` for each `.lambda.ts` handler in your source tree. If this is disabled, you either need to explicitly call `aws_lambda.Function.autoDiscover()` or define a `new aws_lambda.Function()` for each handler.
__*Default*__: true +**lambdaExtensionAutoDiscover**?🔹 | boolean | Automatically adds an `awscdk.LambdaExtension` for each `.lambda-extension.ts` entrypoint in your source tree. If this is disabled, you can manually add an `awscdk.AutoDiscover` component to your project.
__*Default*__: true **lambdaOptions**?🔹 | [awscdk.LambdaFunctionCommonOptions](#projen-awscdk-lambdafunctioncommonoptions) | Common options for all AWS Lambda functions.
__*Default*__: default options **libdir**?🔹 | string | Typescript artifacts output directory.
__*Default*__: "lib" **license**?🔹 | string | License's SPDX identifier.
__*Default*__: "Apache-2.0" @@ -11929,6 +12001,7 @@ Name | Type | Description **jsiiReleaseVersion**?🔹 | string | Version requirement of `publib` which is used to publish modules to npm.
__*Default*__: "latest" **keywords**?🔹 | Array | Keywords to include in `package.json`.
__*Optional*__ **lambdaAutoDiscover**?🔹 | boolean | Automatically adds an `awscdk.LambdaFunction` for each `.lambda.ts` handler in your source tree. If this is disabled, you can manually add an `awscdk.AutoDiscover` component to your project.
__*Default*__: true +**lambdaExtensionAutoDiscover**?🔹 | boolean | Automatically adds an `awscdk.LambdaExtension` for each `.lambda-extension.ts` entrypoint in your source tree. If this is disabled, you can manually add an `awscdk.AutoDiscover` component to your project.
__*Default*__: true **lambdaOptions**?🔹 | [awscdk.LambdaFunctionCommonOptions](#projen-awscdk-lambdafunctioncommonoptions) | Common options for all AWS Lambda functions.
__*Default*__: default options **libdir**?🔹 | string | Typescript artifacts output directory.
__*Default*__: "lib" **license**?🔹 | string | License's SPDX identifier.
__*Default*__: "Apache-2.0" @@ -12132,6 +12205,7 @@ Name | Type | Description **jsiiReleaseVersion**?⚠️ | string | Version requirement of `publib` which is used to publish modules to npm.
__*Default*__: "latest" **keywords**?⚠️ | Array | Keywords to include in `package.json`.
__*Optional*__ **lambdaAutoDiscover**?⚠️ | boolean | Automatically adds an `aws_lambda.Function` for each `.lambda.ts` handler in your source tree. If this is disabled, you either need to explicitly call `aws_lambda.Function.autoDiscover()` or define a `new aws_lambda.Function()` for each handler.
__*Default*__: true +**lambdaExtensionAutoDiscover**?⚠️ | boolean | Automatically adds an `awscdk.LambdaExtension` for each `.lambda-extension.ts` entrypoint in your source tree. If this is disabled, you can manually add an `awscdk.AutoDiscover` component to your project.
__*Default*__: true **lambdaOptions**?⚠️ | [awscdk.LambdaFunctionCommonOptions](#projen-awscdk-lambdafunctioncommonoptions) | Common options for all AWS Lambda functions.
__*Default*__: default options **libdir**?⚠️ | string | Typescript artifacts output directory.
__*Default*__: "lib" **license**?⚠️ | string | License's SPDX identifier.
__*Default*__: "Apache-2.0" @@ -12282,7 +12356,56 @@ Name | Type | Description **cdkDeps**🔹 | [awscdk.AwsCdkDeps](#projen-awscdk-awscdkdeps) | AWS CDK dependency manager. **srcdir**🔹 | string | Project source tree (relative to project output directory). **tsconfigPath**🔹 | string | Path to the tsconfig file to use for integration tests. -**lambdaOptions**?🔹 | [awscdk.LambdaFunctionCommonOptions](#projen-awscdk-lambdafunctioncommonoptions) | Options for auto-discovery of AWS Lambda functions.
__*Optional*__ +**lambdaOptions**?🔹 | [awscdk.LambdaFunctionCommonOptions](#projen-awscdk-lambdafunctioncommonoptions) | Options for AWS Lambda functions.
__*Optional*__ + + + +## struct LambdaExtensionAutoDiscoverOptions 🔹 + + +Options for `LambdaExtensionAutoDiscover`. + + + +Name | Type | Description +-----|------|------------- +**cdkDeps**🔹 | [awscdk.AwsCdkDeps](#projen-awscdk-awscdkdeps) | AWS CDK dependency manager. +**srcdir**🔹 | string | Project source tree (relative to project output directory). +**tsconfigPath**🔹 | string | Path to the tsconfig file to use for integration tests. +**lambdaExtensionOptions**?🔹 | [awscdk.LambdaExtensionCommonOptions](#projen-awscdk-lambdaextensioncommonoptions) | Options for lambda extensions.
__*Optional*__ + + + +## struct LambdaExtensionCommonOptions 🔹 + + +Common options for creating lambda extensions. + + + +Name | Type | Description +-----|------|------------- +**bundlingOptions**?🔹 | [javascript.BundlingOptions](#projen-javascript-bundlingoptions) | Bundling options for this AWS Lambda extension.
__*Default*__: defaults +**compatibleRuntimes**?🔹 | Array<[awscdk.LambdaRuntime](#projen-awscdk-lambdaruntime)> | The extension's compatible runtimes.
__*Optional*__ + + + +## struct LambdaExtensionOptions 🔹 + + +Options for creating lambda extensions. + + + +Name | Type | Description +-----|------|------------- +**cdkDeps**🔹 | [awscdk.AwsCdkDeps](#projen-awscdk-awscdkdeps) | AWS CDK dependency manager. +**entrypoint**🔹 | string | A path from the project root directory to a TypeScript file which contains the AWS Lambda extension entrypoint (stand-alone script). +**bundlingOptions**?🔹 | [javascript.BundlingOptions](#projen-javascript-bundlingoptions) | Bundling options for this AWS Lambda extension.
__*Default*__: defaults +**compatibleRuntimes**?🔹 | Array<[awscdk.LambdaRuntime](#projen-awscdk-lambdaruntime)> | The extension's compatible runtimes.
__*Optional*__ +**constructFile**?🔹 | string | The name of the generated TypeScript source file.
__*Default*__: The name of the entrypoint file, with the `-layer-version.ts` suffix instead of `.lambda-extension.ts`. +**constructName**?🔹 | string | The name of the generated `lambda.LayerVersion` subclass.
__*Default*__: A pascal cased version of the name of the entrypoint file, with the extension `LayerVersion` (e.g. `AppConfigLayerVersion`). +**name**?🔹 | string | Name of the extension.
__*Default*__: Derived from the entrypoint filename. @@ -14478,7 +14601,9 @@ Name | Type | Description -----|------|------------- **platform**🔹 | string | esbuild platform. **target**🔹 | string | esbuild target. +**executable**?🔹 | boolean | Mark the output file as executable.
__*Default*__: false **externals**?🔹 | Array | You can mark a file or a package as external to exclude it from your build.
__*Default*__: [] +**outfile**?🔹 | string | Bundler output path relative to the asset's output directory.
__*Default*__: "index.js" **sourcemap**?🔹 | boolean | Include a source map in the bundle.
__*Default*__: false **watchTask**?🔹 | boolean | In addition to the `bundle:xyz` task, creates `bundle:xyz:watch` task which will invoke the same esbuild command with the `--watch` flag.
__*Default*__: true @@ -14495,6 +14620,7 @@ __Obtainable from__: [Bundler](#projen-javascript-bundler).[addBundle](#projen-j Name | Type | Description -----|------|------------- **bundleTask**🔹 | [Task](#projen-task) | The task that produces this bundle. +**outdir**🔹 | string | Base directory containing the output file (relative to project root). **outfile**🔹 | string | Location of the output file (relative to project root). **watchTask**?🔹 | [Task](#projen-task) | The "watch" task for this bundle.
__*Optional*__ diff --git a/docs/awscdk.md b/docs/awscdk.md index 920bcaf175f..060e1afff8f 100644 --- a/docs/awscdk.md +++ b/docs/awscdk.md @@ -101,6 +101,142 @@ new awscdk.LambdaFunction(p, { }); ``` +## AWS Lambda Extensions + +An AWS [Lambda Extension][lambda-extensions-blog] is a way to integrate your +preferred development, monitoring, observability, and governance tools with +AWS Lambda. + +Functionally, AWS [Lambda Extensions][lambda-extensions-blog] are long-running +executable files that reside in the `extensions` subdirectory of your code +asset. AWS Lambda executes all extensions before starting your handler's main +process. These AWS Lambda Extensions interact with your function's main process +and the [Lambda Extension API][lambda-extensions-api] to integrate with tools +outside the Lambda environment. Projen helps with bundling and preparing your +code as reusable Lambda Layers. + +[lambda-extensions-blog]: https://aws.amazon.com/blogs/aws/getting-started-with-using-your-favorite-operational-tools-on-aws-lambda-extensions-are-now-generally-available/ +[lambda-extensions-api]: https://docs.aws.amazon.com/lambda/latest/dg/runtimes-extensions-api.html + +To create an AWS Lambda Extension with Projen: + +- Create a file in your project's source tree called + `my-extension.lambda-extension.ts` +- Run `npx projen` +- Projen will automatically discover this file, generating an AWS Lambda Layer + Version named `MyExtensionLayerVersion` in a file named + `my-extension-layer-version.ts`. +- Now you can instantiate `MyExtensionLayerVersion` and add it to your Lambda + functions. + +Offical AWS extension examples are available in the [AWS Samples][ext-samples] +repository. + +[ext-samples]: https://github.com/aws-samples/aws-lambda-extensions + +**Example of an extension:** + +A skeleton for a Lambda extension follows below. Comments with `TODO` describe +locations where you can provide your custom functionality. + +```ts +#!/usr/bin/env node +// ^ Don't forget this shebang - Lambda executes the bundled version of this +// file directly and doesn't otherwise know it's a node script. + +import { basename } from 'path'; + +// This example uses the `got` HTTP client and assumes that you have included +// `got` in your `devDependencies`. But, you can use any HTTP client you like. +import got from 'got'; + +/** + * Your Lambda Extension's main loop + */ +async function main() { + const extensionInfo = await registerExtension([ + ExtensionEventType.SHUTDOWN, + ExtensionEventType.INVOKE, + ]); + + // TODO: Put your initialization code here. You can do things like + // testing a connection to your external tooling here. + + while (true) { + const event = await getNextEvent(extensionInfo.extensionId); + + switch (event.eventType) { + case ExtensionEventType.SHUTDOWN: + // TODO: Do something when the lambda extension is being + // shut down. You might do things here like de-registering + // your extension from your external tooling. + return 0; + + case ExtensionEventType.INVOKE: + // TODO: Do something every time your function is invoked, + // such as re-establishing a connection with your external + // tooling after the Lambda has thawed from a period of + // freezing due to inactivity. + break; + + default: + console.log(`Unhandled event type ${event.eventType}`); + } + } +} + +const EXTENSION_API_BASE_URL = `http://${process.env.AWS_LAMBDA_RUNTIME_API}/2020-01-01/extension`; + +enum ExtensionEventType { + INVOKE = 'INVOKE', + SHUTDOWN = 'SHUTDOWN', +} + +interface ExtensionEvent { + readonly eventType: ExtensionEventType; + // For complete event structures, see: + // https://docs.aws.amazon.com/lambda/latest/dg/runtimes-extensions-api.html +} + +async function registerExtension(events: ExtensionEventType[]) { + // Do not set a timeout on the GET call, as the extension can be suspended + // for a period of time until there is an event to return. + const res = await got.post(`${EXTENSION_API_BASE_URL}/register`, { + json: { events }, + headers: { + 'Lambda-Extension-Name': basename(__filename), + }, + }); + + const header = res.headers['lambda-extension-identifier']; + const extensionId = Array.isArray(header) ? header[0] : header; + const json = JSON.parse(res.body); + + return { + extensionId, + functionName: json.functionName as string, + functionVersion: json.functionVersion as string, + }; +} + +function getNextEvent(extensionId: string): Promise { + return got(`${EXTENSION_API_BASE_URL}/event/next`, { + headers: { + 'Lambda-Extension-Identifier': extensionId, + }, + }).json(); +} + +main() + .then(statusCode => { + process.exit(statusCode); + }) + .catch(e => { + console.error(e); + process.exit(1); + }); +``` + ## Integration Snapshot Tests Files in the `test/` tree with the `.integ.ts` suffix are recognized as diff --git a/src/awscdk/auto-discover.ts b/src/awscdk/auto-discover.ts index 47b48d10f59..7a71e1f812b 100644 --- a/src/awscdk/auto-discover.ts +++ b/src/awscdk/auto-discover.ts @@ -10,7 +10,14 @@ import { IntegrationTest, IntegrationTestCommonOptions, } from "./integration-test"; -import { TYPESCRIPT_LAMBDA_EXT } from "./internal"; +import { + TYPESCRIPT_LAMBDA_EXT, + TYPESCRIPT_LAMBDA_EXTENSION_EXT, +} from "./internal"; +import { + LambdaExtension, + LambdaExtensionCommonOptions, +} from "./lambda-extension"; import { LambdaFunction, LambdaFunctionCommonOptions } from "./lambda-function"; /** @@ -68,7 +75,7 @@ export interface LambdaAutoDiscoverOptions extends AutoDiscoverCommonOptions { readonly srcdir: string; /** - * Options for auto-discovery of AWS Lambda functions. + * Options for AWS Lambda functions. */ readonly lambdaOptions?: LambdaFunctionCommonOptions; } @@ -93,11 +100,49 @@ export class LambdaAutoDiscover extends AutoDiscoverBase { } } +/** + * Options for `LambdaExtensionAutoDiscover` + */ +export interface LambdaExtensionAutoDiscoverOptions + extends AutoDiscoverCommonOptions { + /** + * Project source tree (relative to project output directory). + */ + readonly srcdir: string; + + /** + * Options for lambda extensions. + */ + readonly lambdaExtensionOptions?: LambdaExtensionCommonOptions; +} + +/** + * Creates Lambda Extensions from entrypoints discovered in the project's + * source tree. + */ +export class LambdaExtensionAutoDiscover extends AutoDiscoverBase { + constructor(project: Project, options: LambdaExtensionAutoDiscoverOptions) { + super(project, { + projectdir: options.srcdir, + extension: TYPESCRIPT_LAMBDA_EXTENSION_EXT, + }); + + for (const entrypoint of this.entrypoints) { + new LambdaExtension(this.project, { + entrypoint, + cdkDeps: options.cdkDeps, + ...options.lambdaExtensionOptions, + }); + } + } +} + /** * Options for `AutoDiscover` */ export interface AutoDiscoverOptions extends LambdaAutoDiscoverOptions, + LambdaExtensionAutoDiscoverOptions, IntegrationTestAutoDiscoverOptions { /** * Auto-discover lambda functions. @@ -106,6 +151,13 @@ export interface AutoDiscoverOptions */ readonly lambdaAutoDiscover?: boolean; + /** + * Auto-discover lambda extensions. + * + * @default true + */ + readonly lambdaExtensionAutoDiscover?: boolean; + /** * Auto-discover integration tests. * @@ -131,6 +183,15 @@ export class AutoDiscover extends Component { }); } + if (options.lambdaExtensionAutoDiscover ?? true) { + new LambdaExtensionAutoDiscover(this.project, { + cdkDeps: options.cdkDeps, + tsconfigPath: options.tsconfigPath, + srcdir: options.srcdir, + lambdaExtensionOptions: options.lambdaExtensionOptions, + }); + } + if (options.integrationTestAutoDiscover ?? true) { new IntegrationTestAutoDiscover(this.project, { cdkDeps: options.cdkDeps, diff --git a/src/awscdk/awscdk-app-ts.ts b/src/awscdk/awscdk-app-ts.ts index 1fe1198c091..1dcd5ecc75d 100644 --- a/src/awscdk/awscdk-app-ts.ts +++ b/src/awscdk/awscdk-app-ts.ts @@ -31,6 +31,15 @@ export interface AwsCdkTypeScriptAppOptions */ readonly lambdaAutoDiscover?: boolean; + /** + * Automatically adds an `awscdk.LambdaExtension` for each `.lambda-extension.ts` + * entrypoint in your source tree. If this is disabled, you can manually add an + * `awscdk.AutoDiscover` component to your project + * + * @default true + */ + readonly lambdaExtensionAutoDiscover?: boolean; + /** * Automatically discovers and creates integration tests for each `.integ.ts` * file in under your test directory. @@ -154,6 +163,7 @@ export class AwsCdkTypeScriptApp extends TypeScriptAppProject { tsconfigPath: this.tsconfigDev.fileName, cdkDeps: this.cdkDeps, lambdaAutoDiscover: options.lambdaAutoDiscover ?? true, + lambdaExtensionAutoDiscover: options.lambdaExtensionAutoDiscover ?? true, integrationTestAutoDiscover: options.integrationTestAutoDiscover ?? true, }); } diff --git a/src/awscdk/awscdk-construct.ts b/src/awscdk/awscdk-construct.ts index 37c91c6a854..c5ba2265776 100644 --- a/src/awscdk/awscdk-construct.ts +++ b/src/awscdk/awscdk-construct.ts @@ -22,6 +22,15 @@ export interface AwsCdkConstructLibraryOptions */ readonly lambdaAutoDiscover?: boolean; + /** + * Automatically adds an `awscdk.LambdaExtension` for each `.lambda-extension.ts` + * entrypoint in your source tree. If this is disabled, you can manually add an + * `awscdk.AutoDiscover` component to your project + * + * @default true + */ + readonly lambdaExtensionAutoDiscover?: boolean; + /** * Automatically discovers and creates integration tests for each `.integ.ts` * file in under your test directory. @@ -80,6 +89,7 @@ export class AwsCdkConstructLibrary extends ConstructLibrary { tsconfigPath: this.tsconfigDev.fileName, cdkDeps: this.cdkDeps, lambdaAutoDiscover: options.lambdaAutoDiscover ?? true, + lambdaExtensionAutoDiscover: options.lambdaExtensionAutoDiscover ?? true, integrationTestAutoDiscover: options.integrationTestAutoDiscover ?? true, }); } diff --git a/src/awscdk/index.ts b/src/awscdk/index.ts index 4a2b558982b..d62163797ee 100644 --- a/src/awscdk/index.ts +++ b/src/awscdk/index.ts @@ -10,3 +10,4 @@ export * from "./cdk-config"; export * from "./cdk-tasks"; export * from "./integration-test"; export * from "./lambda-function"; +export * from "./lambda-extension"; diff --git a/src/awscdk/internal.ts b/src/awscdk/internal.ts index 11aa9b2557d..0b013ca91fe 100644 --- a/src/awscdk/internal.ts +++ b/src/awscdk/internal.ts @@ -1,3 +1,5 @@ +import { sep, posix } from "path"; + /** * Feature flags as of v1.130.0 */ @@ -21,3 +23,15 @@ export const FEATURE_FLAGS = [ * Suffix for AWS Lambda handlers. */ export const TYPESCRIPT_LAMBDA_EXT = ".lambda.ts"; + +/** + * Suffix for AWS Lambda Extensions. + */ +export const TYPESCRIPT_LAMBDA_EXTENSION_EXT = ".lambda-extension.ts"; + +/** + * Converts the given path string to posix if it wasn't already. + */ +export function convertToPosixPath(p: string) { + return p.split(sep).join(posix.sep); +} diff --git a/src/awscdk/lambda-extension.ts b/src/awscdk/lambda-extension.ts new file mode 100644 index 00000000000..a4e0a121888 --- /dev/null +++ b/src/awscdk/lambda-extension.ts @@ -0,0 +1,229 @@ +import { basename, dirname, join, relative } from "path"; +import { pascal } from "case"; +import { Component } from "../component"; +import { Bundler, BundlingOptions, Eslint } from "../javascript"; +import { Project } from "../project"; +import { SourceCode } from "../source-code"; +import { AwsCdkDeps } from "./awscdk-deps"; +import { + convertToPosixPath, + TYPESCRIPT_LAMBDA_EXTENSION_EXT, +} from "./internal"; +import { LambdaRuntime } from "./lambda-function"; + +/** + * Common options for creating lambda extensions. + */ +export interface LambdaExtensionCommonOptions { + /** + * The extension's compatible runtimes. + */ + readonly compatibleRuntimes?: LambdaRuntime[]; + + /** + * Bundling options for this AWS Lambda extension. + * + * If not specified the default bundling options specified for the project + * `Bundler` instance will be used. + * + * @default - defaults + */ + readonly bundlingOptions?: BundlingOptions; +} + +/** + * Options for creating lambda extensions. + */ +export interface LambdaExtensionOptions extends LambdaExtensionCommonOptions { + /** + * Name of the extension + * + * @default - Derived from the entrypoint filename. + */ + readonly name?: string; + + /** + * A path from the project root directory to a TypeScript file which contains + * the AWS Lambda extension entrypoint (stand-alone script). + * + * This is relative to the root directory of the project. + * + * @example "src/subdir/foo.lambda-extension.ts" + */ + readonly entrypoint: string; + + /** + * AWS CDK dependency manager. + */ + readonly cdkDeps: AwsCdkDeps; + + /** + * The name of the generated TypeScript source file. This file should also be + * under the source tree. + * + * @default - The name of the entrypoint file, with the `-layer-version.ts` + * suffix instead of `.lambda-extension.ts`. + */ + readonly constructFile?: string; + + /** + * The name of the generated `lambda.LayerVersion` subclass. + * + * @default - A pascal cased version of the name of the entrypoint file, with + * the extension `LayerVersion` (e.g. `AppConfigLayerVersion`). + */ + readonly constructName?: string; +} + +/** + * Create a Lambda Extension + */ +export class LambdaExtension extends Component { + constructor(project: Project, options: LambdaExtensionOptions) { + super(project); + + const basePath = join( + dirname(options.entrypoint), + basename(options.entrypoint, TYPESCRIPT_LAMBDA_EXTENSION_EXT) + ); + + const name = options.name ?? basename(basePath); + + const bundler = Bundler.of(project); + if (!bundler) { + throw new Error( + "No bundler found. Please add a Bundler component to your project." + ); + } + + const compatibleRuntimes = options.compatibleRuntimes ?? [ + LambdaRuntime.NODEJS_14_X, + LambdaRuntime.NODEJS_12_X, + ]; + + if (compatibleRuntimes.length === 0) { + throw new Error("Compatible runtimes must include at least one runtime"); + } + + // Use the lowest runtime version to bundle + const [bundlerRuntime] = compatibleRuntimes.sort((a, b) => + a.functionRuntime.localeCompare(b.functionRuntime) + ); + + // Allow extension code to import dev-deps since they are only needed + // during bundling + const eslint = Eslint.of(project); + eslint?.allowDevDeps(options.entrypoint); + + const bundle = bundler.addBundle(options.entrypoint, { + platform: bundlerRuntime.esbuildPlatform, + target: bundlerRuntime.esbuildTarget, + externals: ["aws-sdk"], + outfile: `extensions/${name}`, + // Make the output executable because Lambda expects to run + // extensions as stand-alone programs alongside the main lambda + // process. + executable: true, + ...options.bundlingOptions, + }); + + const constructFile = + options.constructFile ?? `${basePath}-layer-version.ts`; + + new LambdaLayerConstruct(project, { + constructFile: constructFile, + constructName: options.constructName, + assetDir: bundle.outdir, + compatibleRuntimes: compatibleRuntimes, + description: `Provides a Lambda Extension \`${name}\` from ${convertToPosixPath( + options.entrypoint + )}`, + cdkDeps: options.cdkDeps, + }); + } +} + +interface LambdaLayerConstructOptions { + readonly assetDir: string; + readonly compatibleRuntimes: LambdaRuntime[]; + readonly description: string; + readonly constructFile: string; + readonly constructName?: string; + readonly cdkDeps: AwsCdkDeps; +} + +class LambdaLayerConstruct extends SourceCode { + constructor(project: Project, options: LambdaLayerConstructOptions) { + super(project, options.constructFile); + + const src = this; + const cdkDeps = options.cdkDeps; + + const constructName = + options.constructName ?? pascal(basename(options.constructFile, ".ts")); + const propsType = `${constructName}Props`; + + const assetDir = relative(dirname(options.constructFile), options.assetDir); + + if (src.marker) { + src.line(`// ${src.marker}`); + } + src.line("import * as path from 'path';"); + + if (cdkDeps.cdkMajorVersion === 1) { + src.line("import * as lambda from '@aws-cdk/aws-lambda';"); + src.line("import { Construct } from '@aws-cdk/core';"); + cdkDeps.addV1Dependencies("@aws-cdk/aws-lambda"); + cdkDeps.addV1Dependencies("@aws-cdk/core"); + } else { + src.line("import * as lambda from 'aws-cdk-lib/aws-lambda';"); + src.line("import { Construct } from 'constructs';"); + } + + src.line(); + + src.line("/**"); + src.line(` * Props for ${constructName}`); + src.line(" */"); + src.open( + `export interface ${propsType} extends lambda.LayerVersionOptions {` + ); + src.close("}"); + src.line(); + + src.line("/**"); + src.line(` * ${options.description}`); + src.line(" */"); + src.open(`export class ${constructName} extends lambda.LayerVersion {`); + src.open( + `constructor(scope: Construct, id: string, props?: ${propsType}) {` + ); + + src.open("super(scope, id, {"); + src.line(`description: ${encodeCodeString(options.description)},`); + src.line("...props,"); + + src.open("compatibleRuntimes: ["); + for (const runtime of options.compatibleRuntimes) { + src.line(`lambda.Runtime.${runtime.functionRuntime},`); + } + src.close("],"); + + src.open(`code: lambda.Code.fromAsset(path.join(__dirname,`); + src.line(`${encodeCodeString(convertToPosixPath(assetDir))})),`); + src.close(); + src.close("});"); + + src.close("}"); + src.close("}"); + } +} + +/** + * Encodes a string for embedding in source code. + */ +function encodeCodeString(value: string) { + const json = JSON.stringify(value); + const escapedString = json.substring(1, json.length - 1).replace(/'/g, "\\'"); + return `'${escapedString}'`; +} diff --git a/src/awscdk/lambda-function.ts b/src/awscdk/lambda-function.ts index 20f4d504788..8b011dbd73b 100644 --- a/src/awscdk/lambda-function.ts +++ b/src/awscdk/lambda-function.ts @@ -1,11 +1,11 @@ -import { basename, dirname, extname, join, relative, sep, posix } from "path"; +import { basename, dirname, extname, join, relative } from "path"; import { pascal } from "case"; import { Component } from "../component"; import { Bundler, BundlingOptions, Eslint } from "../javascript"; import { Project } from "../project"; import { SourceCode } from "../source-code"; import { AwsCdkDeps } from "./awscdk-deps"; -import { TYPESCRIPT_LAMBDA_EXT } from "./internal"; +import { convertToPosixPath, TYPESCRIPT_LAMBDA_EXT } from "./internal"; /** * Common options for `LambdaFunction`. Applies to all functions in @@ -279,10 +279,3 @@ export class LambdaRuntime { public readonly esbuildTarget: string ) {} } - -/** - * Converts the given path string to posix if it wasn't already. - */ -function convertToPosixPath(p: string) { - return p.split(sep).join(posix.sep); -} diff --git a/src/javascript/bundler.ts b/src/javascript/bundler.ts index 013bed37934..6c3b56f340c 100644 --- a/src/javascript/bundler.ts +++ b/src/javascript/bundler.ts @@ -102,7 +102,8 @@ export class Bundler extends Component { public addBundle(entrypoint: string, options: AddBundleOptions): Bundle { const name = renderBundleName(entrypoint); - const outfile = join(this.bundledir, name, "index.js"); + const outdir = join(this.bundledir, name); + const outfile = join(outdir, options.outfile ?? "index.js"); const args = [ "esbuild", "--bundle", @@ -128,6 +129,10 @@ export class Bundler extends Component { this.bundleTask.spawn(bundleTask); + if (options.executable ?? false) { + bundleTask.exec(`chmod +x ${outfile}`); + } + let watchTask; const watch = options.watchTask ?? true; if (watch) { @@ -140,6 +145,7 @@ export class Bundler extends Component { return { bundleTask: bundleTask, watchTask: watchTask, + outdir: outdir, outfile: outfile, }; } @@ -175,6 +181,11 @@ export interface Bundle { * Location of the output file (relative to project root). */ readonly outfile: string; + + /** + * Base directory containing the output file (relative to project root). + */ + readonly outdir: string; } /** @@ -233,4 +244,16 @@ export interface AddBundleOptions extends BundlingOptions { * @example "node" */ readonly platform: string; + + /** + * Bundler output path relative to the asset's output directory. + * @default "index.js" + */ + readonly outfile?: string; + + /** + * Mark the output file as executable. + * @default false + */ + readonly executable?: boolean; } diff --git a/test/__snapshots__/inventory.test.ts.snap b/test/__snapshots__/inventory.test.ts.snap index 0d219346d54..dfcf982bb43 100644 --- a/test/__snapshots__/inventory.test.ts.snap +++ b/test/__snapshots__/inventory.test.ts.snap @@ -3268,6 +3268,23 @@ Array [ "simpleType": "boolean", "switch": "lambda-auto-discover", }, + Object { + "default": "true", + "docs": "Automatically adds an \`awscdk.LambdaExtension\` for each \`.lambda-extension.ts\` entrypoint in your source tree. If this is disabled, you can manually add an \`awscdk.AutoDiscover\` component to your project.", + "featured": false, + "fullType": Object { + "primitive": "boolean", + }, + "jsonLike": true, + "name": "lambdaExtensionAutoDiscover", + "optional": true, + "parent": "AwsCdkTypeScriptAppOptions", + "path": Array [ + "lambdaExtensionAutoDiscover", + ], + "simpleType": "boolean", + "switch": "lambda-extension-auto-discover", + }, Object { "default": "- default options", "docs": "Common options for all AWS Lambda functions.", @@ -6003,6 +6020,23 @@ Array [ "simpleType": "boolean", "switch": "lambda-auto-discover", }, + Object { + "default": "true", + "docs": "Automatically adds an \`awscdk.LambdaExtension\` for each \`.lambda-extension.ts\` entrypoint in your source tree. If this is disabled, you can manually add an \`awscdk.AutoDiscover\` component to your project.", + "featured": false, + "fullType": Object { + "primitive": "boolean", + }, + "jsonLike": true, + "name": "lambdaExtensionAutoDiscover", + "optional": true, + "parent": "AwsCdkConstructLibraryOptions", + "path": Array [ + "lambdaExtensionAutoDiscover", + ], + "simpleType": "boolean", + "switch": "lambda-extension-auto-discover", + }, Object { "default": "- default options", "docs": "Common options for all AWS Lambda functions.", diff --git a/test/awscdk/__snapshots__/lambda-extension.test.ts.snap b/test/awscdk/__snapshots__/lambda-extension.test.ts.snap new file mode 100644 index 00000000000..8236c1769d2 --- /dev/null +++ b/test/awscdk/__snapshots__/lambda-extension.test.ts.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`simplest LambdaExtension cdk v2 1`] = ` +"// ~~ Generated by projen. To modify, edit .projenrc.js and run \\"npx projen\\". +import * as path from 'path'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import { Construct } from 'constructs'; + +/** + * Props for ExampleLayerVersion + */ +export interface ExampleLayerVersionProps extends lambda.LayerVersionOptions { +} + +/** + * Provides a Lambda Extension \`example\` from src/example.lambda-extension.ts + */ +export class ExampleLayerVersion extends lambda.LayerVersion { + constructor(scope: Construct, id: string, props?: ExampleLayerVersionProps) { + super(scope, id, { + description: 'Provides a Lambda Extension \`example\` from src/example.lambda-extension.ts', + ...props, + compatibleRuntimes: [ + lambda.Runtime.NODEJS_12_X, + lambda.Runtime.NODEJS_14_X, + ], + code: lambda.Code.fromAsset(path.join(__dirname, + '../assets/example.lambda-extension')), + }); + } +}" +`; diff --git a/test/awscdk/lambda-extension.test.ts b/test/awscdk/lambda-extension.test.ts new file mode 100644 index 00000000000..93e371960ac --- /dev/null +++ b/test/awscdk/lambda-extension.test.ts @@ -0,0 +1,199 @@ +import { DependencyType, Testing } from "../../src"; +import { + AwsCdkDeps, + AwsCdkDepsJs, + LambdaExtension, + LambdaRuntime, +} from "../../src/awscdk"; +import { TypeScriptProject } from "../../src/typescript"; + +test("simplest LambdaExtension cdk v2", () => { + const project = new TypeScriptProject({ + name: "hello", + defaultReleaseBranch: "main", + }); + + // WHEN + new LambdaExtension(project, { + cdkDeps: cdkDepsForProject(project, "2.1.0"), + entrypoint: "src/example.lambda-extension.ts", + }); + + // THEN + const snapshot = Testing.synth(project); + + const tasks = snapshot[".projen/tasks.json"].tasks; + const bundleTaskExec = tasks["bundle:example.lambda-extension"].steps[0].exec; + + expect(bundleTaskExec).toContain( + // Outputs `extensions/${name}` dir + '--outfile="assets/example.lambda-extension/extensions/example"' + ); + expect(bundleTaskExec).toContain( + // aws-sdk is external + "--external:aws-sdk" + ); + expect(bundleTaskExec).toContain( + // Supports node12 + '--target="node12"' + ); + + const generatedSource = snapshot["src/example-layer-version.ts"]; + expect(generatedSource).toContain( + [ + "import * as lambda from 'aws-cdk-lib/aws-lambda';", + "import { Construct } from 'constructs';", + ].join("\n") + ); + expect(generatedSource).toContain( + "export interface ExampleLayerVersionProps" + ); + expect(generatedSource).toContain("export class ExampleLayerVersion"); + expect(generatedSource).toContain("Runtime.NODEJS_12_X"); + expect(generatedSource).toContain("Runtime.NODEJS_14_X"); + expect(generatedSource).toMatchSnapshot(); +}); + +test("simplest LambdaExtension cdk v1", () => { + const project = new TypeScriptProject({ + name: "hello", + defaultReleaseBranch: "main", + }); + + // WHEN + new LambdaExtension(project, { + cdkDeps: cdkDepsForProject(project), + entrypoint: "src/example.lambda-extension.ts", + }); + + // THEN + const snapshot = Testing.synth(project); + + const generatedSource = snapshot["src/example-layer-version.ts"]; + expect(generatedSource).toContain( + [ + "import * as lambda from '@aws-cdk/aws-lambda';", + "import { Construct } from '@aws-cdk/core';", + ].join("\n") + ); +}); + +test("changing compatible runtimes", () => { + const project = new TypeScriptProject({ + name: "hello", + defaultReleaseBranch: "main", + }); + + // WHEN + new LambdaExtension(project, { + cdkDeps: cdkDepsForProject(project), + entrypoint: "src/example.lambda-extension.ts", + compatibleRuntimes: [ + LambdaRuntime.NODEJS_14_X, + LambdaRuntime.NODEJS_12_X, + LambdaRuntime.NODEJS_10_X, + ], + }); + + // THEN + const snapshot = Testing.synth(project); + + const bundleTaskExec = + snapshot[".projen/tasks.json"].tasks["bundle:example.lambda-extension"] + .steps[0].exec; + + expect(bundleTaskExec).toContain( + // It picked the lowest compatible runtime + '--target="node10"' + ); + + const generatedSource = snapshot["src/example-layer-version.ts"]; + expect(generatedSource).toContain("Runtime.NODEJS_10_X"); + expect(generatedSource).toContain("Runtime.NODEJS_12_X"); + expect(generatedSource).toContain("Runtime.NODEJS_14_X"); +}); + +test("bundler options", () => { + const project = new TypeScriptProject({ + name: "hello", + defaultReleaseBranch: "main", + }); + + // WHEN + new LambdaExtension(project, { + cdkDeps: cdkDepsForProject(project), + entrypoint: "src/example.lambda-extension.ts", + bundlingOptions: { + externals: ["foo"], + }, + }); + + // THEN + const snapshot = Testing.synth(project); + + const bundleTaskExec = + snapshot[".projen/tasks.json"].tasks["bundle:example.lambda-extension"] + .steps[0].exec; + + expect(bundleTaskExec).toContain( + // `foo` is external + "--external:foo" + ); +}); + +test("changing the extension name", () => { + const project = new TypeScriptProject({ + name: "hello", + defaultReleaseBranch: "main", + }); + + // WHEN + new LambdaExtension(project, { + cdkDeps: cdkDepsForProject(project), + entrypoint: "src/example.lambda-extension.ts", + name: "other", + }); + + // THEN + const snapshot = Testing.synth(project); + + const bundleTaskExec = + snapshot[".projen/tasks.json"].tasks["bundle:example.lambda-extension"] + .steps[0].exec; + + expect(bundleTaskExec).toContain( + // Outputs `extensions/${name}` dir + '--outfile="assets/example.lambda-extension/extensions/other"' + ); +}); + +test("changing construct name and path", () => { + const project = new TypeScriptProject({ + name: "hello", + defaultReleaseBranch: "main", + }); + + // WHEN + new LambdaExtension(project, { + cdkDeps: cdkDepsForProject(project), + entrypoint: "src/example.lambda-extension.ts", + constructName: "Custom", + constructFile: "src/example-extension.ts", + }); + + // THEN + const snapshot = Testing.synth(project); + const generatedSource = snapshot["src/example-extension.ts"]; + expect(generatedSource).toContain("export interface CustomProps"); + expect(generatedSource).toContain("export class Custom"); +}); + +function cdkDepsForProject( + project: TypeScriptProject, + cdkVersion = "1.0.0" +): AwsCdkDeps { + return new AwsCdkDepsJs(project, { + cdkVersion: cdkVersion, + dependencyType: DependencyType.RUNTIME, + }); +}