diff --git a/.github/workflows/common-test.yml b/.github/workflows/common-test.yml index 098bd3ce..adf87e6b 100644 --- a/.github/workflows/common-test.yml +++ b/.github/workflows/common-test.yml @@ -131,6 +131,55 @@ jobs: - name: Test - observability mode run: OBSERVABLE_MODE=true npx vitest --retry 2 test/cdk-esm.test.ts + test-cdk-nested: + runs-on: ubuntu-latest + concurrency: + group: test-cdk-nested + steps: + - uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.node_version }} + registry-url: 'https://registry.npmjs.org' + - name: Install dependencies + run: | + node prepareForTest.js cdk-nested + npm i + - name: Download build artifact + uses: actions/download-artifact@v4 + if: ${{ inputs.mode == 'build' }} + with: + name: dist + path: dist + - name: Install lambda-live-debugger globally + if: ${{ inputs.mode == 'global' }} + run: | + npm i lambda-live-debugger@${{ inputs.version || 'latest' }} -g + working-directory: test + - name: Install lambda-live-debugger locally + if: ${{ inputs.mode == 'local' }} + run: | + npm i lambda-live-debugger@${{ inputs.version || 'latest' }} + working-directory: test + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: eu-west-1 + role-to-assume: ${{ secrets.AWS_ROLE }} + role-session-name: GitHubActions + - name: Destroy + run: npm run destroy + working-directory: test/cdk-nested + continue-on-error: true + - name: Deploy + run: npm run deploy + working-directory: test/cdk-nested + - name: Test + run: npx vitest --retry 2 test/cdk-nested.test.ts + - name: Test - observability mode + run: OBSERVABLE_MODE=true npx vitest --retry 2 test/cdk-nested.test.ts + test-sls-basic: runs-on: ubuntu-latest concurrency: @@ -484,6 +533,59 @@ jobs: - name: Test - observability mode run: OBSERVABLE_MODE=true npx vitest --retry 2 test/sam-basic.test.ts + test-sam-nested: + runs-on: ubuntu-latest + concurrency: + group: test-sam-nested + steps: + - uses: actions/checkout@v4 + - uses: aws-actions/setup-sam@v2 + with: + use-installer: true + token: ${{ secrets.GITHUB_TOKEN }} + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.node_version }} + registry-url: 'https://registry.npmjs.org' + - name: Install dependencies + run: | + node prepareForTest.js sam-nested + npm i + - name: Download build artifact + uses: actions/download-artifact@v4 + if: ${{ inputs.mode == 'build' }} + with: + name: dist + path: dist + - name: Install lambda-live-debugger globally + if: ${{ inputs.mode == 'global' }} + run: | + npm i lambda-live-debugger@${{ inputs.version || 'latest' }} -g + working-directory: test + - name: Install lambda-live-debugger locally + if: ${{ inputs.mode == 'local' }} + run: | + npm i lambda-live-debugger@${{ inputs.version || 'latest' }} + working-directory: test + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: eu-west-1 + role-to-assume: ${{ secrets.AWS_ROLE }} + role-session-name: GitHubActions + - name: Destroy + run: npm run destroy + working-directory: test/sam-nested + continue-on-error: true + - name: Deploy + run: npm run deploy + working-directory: test/sam-nested + - name: Test + run: npx vitest --retry 2 test/sam-nested.test.ts + - name: Test - observability mode + run: OBSERVABLE_MODE=true npx vitest --retry 2 test/sam-nested.test.ts + test-sam-alt: runs-on: ubuntu-latest concurrency: diff --git a/.vscode/launch.json b/.vscode/launch.json index a4acbe13..5841916a 100755 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -230,6 +230,26 @@ "type": "node", "cwd": "${workspaceRoot}/test/sam-basic" }, + { + "name": "LLDebugger - SAM nested", + "program": "${workspaceRoot}/node_modules/tsx/dist/cli.mjs", + "args": ["../../src/lldebugger.ts", "--config-env=test"], + "request": "launch", + "skipFiles": ["/**"], + "console": "integratedTerminal", + "type": "node", + "cwd": "${workspaceRoot}/test/sam-nested" + }, + { + "name": "LLDebugger - SAM nested - observability", + "program": "${workspaceRoot}/node_modules/tsx/dist/cli.mjs", + "args": ["../../src/lldebugger.ts", "--config-env=test", "-o"], + "request": "launch", + "skipFiles": ["/**"], + "console": "integratedTerminal", + "type": "node", + "cwd": "${workspaceRoot}/test/sam-nested" + }, { "name": "LLDebugger - SAM alt", "program": "${workspaceRoot}/node_modules/tsx/dist/cli.mjs", diff --git a/package-lock.json b/package-lock.json index 355d4cce..56cfc2ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "test/osls-esbuild", "test/osls-esbuild-cjs", "test/sam-basic", + "test/sam-nested", "test/sam-alt", "test/terraform-basic", "test/opentofu-basic" @@ -40458,6 +40459,10 @@ "resolved": "test/sam-basic", "link": true }, + "node_modules/sam-nested": { + "resolved": "test/sam-nested", + "link": true + }, "node_modules/sax": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", @@ -46318,6 +46323,16 @@ "@types/aws-lambda": "^8.10.159" } }, + "test/sam-nested": { + "version": "0.0.1", + "dependencies": { + "@aws-sdk/client-sts": "^3.936.0" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.5", + "@types/aws-lambda": "^8.10.159" + } + }, "test/sls-basic": { "version": "1.0.0", "license": "ISC", diff --git a/package.json b/package.json index badee595..7fe9752d 100755 --- a/package.json +++ b/package.json @@ -70,6 +70,8 @@ "test-osls-esbuild-esm-observable": "npm run build && RUN_TEST_FROM_CLI=true OBSERVABLE_MODE=true vitest run test/osls-esbuild-esm.test.ts", "test-sam-basic": "npm run build && RUN_TEST_FROM_CLI=true vitest run test/sam-basic.test.ts", "test-sam-basic-observable": "npm run build && RUN_TEST_FROM_CLI=true OBSERVABLE_MODE=true vitest run test/sam-basic.test.ts", + "test-sam-nested": "npm run build && RUN_TEST_FROM_CLI=true vitest run test/sam-nested.test.ts", + "test-sam-nested-observable": "npm run build && RUN_TEST_FROM_CLI=true OBSERVABLE_MODE=true vitest run test/sam-nested.test.ts", "test-sam-alt": "npm run build && RUN_TEST_FROM_CLI=true vitest run test/sam-alt.test.ts", "test-sam-alt-observable": "npm run build && RUN_TEST_FROM_CLI=true OBSERVABLE_MODE=true vitest run test/sam-alt.test.ts", "test-terraform-basic": "npm run build && RUN_TEST_FROM_CLI=true vitest run test/terraform-basic.test.ts", @@ -161,6 +163,7 @@ "test/osls-esbuild", "test/osls-esbuild-cjs", "test/sam-basic", + "test/sam-nested", "test/sam-alt", "test/terraform-basic", "test/opentofu-basic" diff --git a/src/cloudFormation.ts b/src/cloudFormation.ts index 5259f4a3..ff8268f9 100644 --- a/src/cloudFormation.ts +++ b/src/cloudFormation.ts @@ -139,11 +139,13 @@ async function getCloudFormationResources( async function getLambdasInStack( stackName: string, awsConfiguration: AwsConfiguration, + stackLogicalId?: string, ): Promise< Array<{ lambdaName: string; logicalId: string; stackName: string; + stackLogicalId: string; }> > { const response = await getCloudFormationResources( @@ -165,6 +167,7 @@ async function getLambdasInStack( lambdaName: resource.PhysicalResourceId!, logicalId: resource.LogicalResourceId!, stackName: stackName, + stackLogicalId: stackLogicalId ?? stackName, }; }) ?? []; @@ -175,6 +178,7 @@ async function getLambdasInStack( const lambdasInNestedStack = await getLambdasInStack( nestedStack.PhysicalResourceId, awsConfiguration, + nestedStack.LogicalResourceId, ); return lambdasInNestedStack; diff --git a/src/frameworks/samFramework.ts b/src/frameworks/samFramework.ts index efef8993..c6f63a26 100755 --- a/src/frameworks/samFramework.ts +++ b/src/frameworks/samFramework.ts @@ -10,6 +10,7 @@ import { CloudFormation } from '../cloudFormation.js'; import { AwsConfiguration } from '../types/awsConfiguration.js'; import { LldConfigBase } from '../types/lldConfig.js'; import { Logger } from '../logger.js'; +import type { Format } from 'esbuild'; /** * Support for AWS SAM framework @@ -102,24 +103,10 @@ export class SamFramework implements IFramework { throw new Error(`Stack name not found in ${samConfigFile}`); } - const samTemplateContent = await fs.readFile( - path.resolve(samTemplateFile), - 'utf-8', + const lambdas = await this.parseLambdasFromTemplate( + samTemplateFile, + stackName, ); - const template = yaml.parse(samTemplateContent); - - const lambdas: any[] = []; - - // get all resources of type AWS::Serverless::Function - for (const resourceName in template.Resources) { - const resource = template.Resources[resourceName]; - if (resource.Type === 'AWS::Serverless::Function') { - lambdas.push({ - Name: resourceName, - ...resource, - }); - } - } const lambdasDiscovered: LambdaResource[] = []; @@ -137,37 +124,33 @@ export class SamFramework implements IFramework { // get tags for each Lambda for (const func of lambdas) { - const handlerFull = path.join( - func.Properties.CodeUri ?? '', - func.Properties.Handler, - ); + const handlerFull = path.join(func.codeUri ?? '', func.handler); const handlerParts = handlerFull.split('.'); const handler = handlerParts[1]; const functionName = lambdasInStack.find( - (lambda) => lambda.logicalId === func.Name, + (lambda) => + lambda.logicalId === func.name && + lambda.stackLogicalId === func.stackLogicalId, )?.lambdaName; if (!functionName) { - throw new Error(`Function name not found for function: ${func.Name}`); + throw new Error(`Function name not found for function: ${func.name}`); } let esBuildOptions: EsBuildOptions | undefined = undefined; let codePath: string | undefined; - if (func.Metadata?.BuildMethod?.toLowerCase() === 'esbuild') { - if (func.Metadata?.BuildProperties?.EntryPoints?.length > 0) { - codePath = path.join( - func.Properties.CodeUri ?? '', - func.Metadata?.BuildProperties?.EntryPoints[0], - ); + if (func.buildMethod?.toLowerCase() === 'esbuild') { + if (func.entryPoints && func.entryPoints.length > 0) { + codePath = path.join(func.codeUri ?? '', func.entryPoints[0]); } esBuildOptions = { - external: func.Metadata?.BuildProperties?.External, - minify: func.Metadata?.BuildProperties?.Minify, - format: func.Metadata?.BuildProperties?.Format, - target: func.Metadata?.BuildProperties?.Target, + external: func.external, + minify: func.minify, + format: func.format, + target: func.target, }; } @@ -221,6 +204,97 @@ export class SamFramework implements IFramework { return lambdasDiscovered; } + + /** + * Recursively parse templates to find all Lambda functions, including nested stacks + * @param templatePath The path to the CloudFormation/SAM template file + * @param stackName The name of the stack this template belongs to (for nested stacks) + */ + private async parseLambdasFromTemplate( + templatePath: string, + stackName: string, + ): Promise { + const resolvedTemplatePath = path.resolve(templatePath); + const templateDir = path.dirname(resolvedTemplatePath); + + let template: any; + try { + const templateContent = await fs.readFile(resolvedTemplatePath, 'utf-8'); + template = yaml.parse(templateContent); + } catch (err: any) { + Logger.warn( + `[SAM] Could not read or parse template at ${templatePath}: ${err.message}`, + ); + return []; + } + + if (!template.Resources) { + return []; + } + + const lambdas: ParsedLambda[] = []; + + for (const resourceName in template.Resources) { + const resource = template.Resources[resourceName]; + + // Check if it's a Lambda function + if (resource.Type === 'AWS::Serverless::Function') { + lambdas.push({ + templatePath, + name: resourceName, + codeUri: resource.Properties?.CodeUri, + handler: resource.Properties?.Handler, + buildMethod: resource.Metadata?.BuildMethod, + entryPoints: resource.Metadata?.BuildProperties?.EntryPoints, + external: resource.Metadata?.BuildProperties?.External, + minify: resource.Metadata?.BuildProperties?.Minify, + format: resource.Metadata?.BuildProperties?.Format as + | Format + | undefined, + target: resource.Metadata?.BuildProperties?.Target, + stackLogicalId: stackName, + }); + } + // Check if it's a nested stack + else if ( + resource.Type === 'AWS::Serverless::Application' || + resource.Type === 'AWS::CloudFormation::Stack' + ) { + const nestedTemplateLocation = + resource.Properties?.Location ?? resource.Properties?.TemplateURL; + if (nestedTemplateLocation) { + const nestedTemplatePath = path.resolve( + templateDir, + nestedTemplateLocation, + ); + + const nestedLambdas = await this.parseLambdasFromTemplate( + nestedTemplatePath, + resourceName, + ); + lambdas.push(...nestedLambdas); + } + } + } + + Logger.verbose(JSON.stringify(lambdas, null, 2)); + + return lambdas; + } } export const samFramework = new SamFramework(); + +type ParsedLambda = { + templatePath: string; + name: string; + codeUri?: string; + handler: string; + buildMethod?: string; + entryPoints?: string[]; + external?: string[]; + minify?: boolean; + format?: Format; + target?: string; + stackLogicalId: string; +}; diff --git a/test/sam-nested.test.ts b/test/sam-nested.test.ts new file mode 100644 index 00000000..3b930ee2 --- /dev/null +++ b/test/sam-nested.test.ts @@ -0,0 +1,222 @@ +import { expect, test, describe, beforeAll, afterAll } from 'vitest'; +import { ChildProcess } from 'child_process'; +import fs from 'fs/promises'; +import { startDebugger } from './utils/startDebugger.js'; +import { expectInfraRemoved } from './utils/expectInfraRemoved.js'; +import { expectInfraDeployed } from './utils/expectInfraDeployed.js'; +import { removeInfra } from './utils/removeInfra.js'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import { callLambda } from './utils/callLambda.js'; +import { getSamplePayload } from './utils/getSamplePayload.js'; +import { validateLocalResponse } from './utils/validateLocalResponse.js'; +import { getTestProjectFolder } from './utils/getTestProjectFolder.js'; + +export const execAsync = promisify(exec); + +export const observableMode = process.env.OBSERVABLE_MODE === 'true'; + +describe('sam-nested', async () => { + const folder = await getTestProjectFolder('sam-nested'); + let lldProcess: ChildProcess | undefined; + + beforeAll(async () => { + if (process.env.CI === 'true' || process.env.RUN_TEST_FROM_CLI === 'true') { + lldProcess = await startDebugger(folder, ['--config-env=test']); + } + }); + + afterAll(async () => { + // stop the debugger + lldProcess?.kill(); + }); + + test('check infra', async () => { + const lambdaName = await getSamFunctionName( + folder, + 'FunctionNameTestTsCommonJs', + ); + await expectInfraDeployed(lambdaName); + }); + + test('call Lambda - testTsCommonJs', async () => { + const lambdaName = await getSamFunctionName( + folder, + 'FunctionNameTestTsCommonJs', + ); + + const payload = getSamplePayload(lambdaName); + const response = await callLambda(lambdaName, payload); + + expect(response.inputEvent).toEqual(payload); + expect(response.runningLocally).toEqual(!observableMode); + if (observableMode) { + await validateLocalResponse(lambdaName, payload); + } + }); + + test('call Lambda - testTsEsModule', async () => { + const lambdaName = await getSamFunctionName( + folder, + 'FunctionNameTestTsEsModule', + ); + + const payload = getSamplePayload(lambdaName); + const response = await callLambda(lambdaName, payload); + + expect(response.inputEvent).toEqual(payload); + expect(response.runningLocally).toEqual(!observableMode); + if (observableMode) { + await validateLocalResponse(lambdaName, payload); + } + }); + + test('call Lambda - testJsCommonJs', async () => { + const lambdaName = await getSamFunctionName( + folder, + 'FunctionNameTestJsCommonJs', + ); + + const payload = getSamplePayload(lambdaName); + const response = await callLambda(lambdaName, payload); + + expect(response.inputEvent).toEqual(payload); + expect(response.runningLocally).toEqual(!observableMode); + if (observableMode) { + await validateLocalResponse(lambdaName, payload); + } + }); + + test('call Lambda - testJsEsModule', async () => { + const lambdaName = await getSamFunctionName( + folder, + 'FunctionNameTestJsEsModule', + ); + + const payload = getSamplePayload(lambdaName); + const response = await callLambda(lambdaName, payload); + + expect(response.inputEvent).toEqual(payload); + expect(response.runningLocally).toEqual(!observableMode); + if (observableMode) { + await validateLocalResponse(lambdaName, payload); + } + }); + + test('call Lambda - testTsCommonJsFromNestedStack', async () => { + const lambdaName = await getSamFunctionName( + folder, + 'FunctionNameTestTsCommonJsFromNestedStack', + ); + + const payload = getSamplePayload(lambdaName); + const response = await callLambda(lambdaName, payload); + + expect(response.inputEvent).toEqual(payload); + expect(response.runningLocally).toEqual(!observableMode); + if (observableMode) { + await validateLocalResponse(lambdaName, payload); + } + }); + + test('call Lambda - testTsEsModuleFromNestedStack', async () => { + const lambdaName = await getSamFunctionName( + folder, + 'FunctionNameTestTsEsModuleFromNestedStack', + ); + + const payload = getSamplePayload(lambdaName); + const response = await callLambda(lambdaName, payload); + + expect(response.inputEvent).toEqual(payload); + expect(response.runningLocally).toEqual(!observableMode); + if (observableMode) { + await validateLocalResponse(lambdaName, payload); + } + }); + + test('call Lambda - testJsCommonJsFromNestedStack', async () => { + const lambdaName = await getSamFunctionName( + folder, + 'FunctionNameTestJsCommonJsFromNestedStack', + ); + + const payload = getSamplePayload(lambdaName); + const response = await callLambda(lambdaName, payload); + + expect(response.runningLocally).toEqual(!observableMode); + expect(response.inputEvent).toEqual(payload); + if (observableMode) { + await validateLocalResponse(lambdaName, payload); + } + }); + + test('call Lambda - testTsCommonJsNested', async () => { + const lambdaName = await getSamFunctionName( + folder, + 'FunctionNameTestTsCommonJsNested', + ); + + const payload = getSamplePayload(lambdaName); + const response = await callLambda(lambdaName, payload); + + expect(response.inputEvent).toEqual(payload); + expect(response.runningLocally).toEqual(!observableMode); + if (observableMode) { + await validateLocalResponse(lambdaName, payload); + } + }); + + test('call Lambda - testTsEsModuleNested', async () => { + const lambdaName = await getSamFunctionName( + folder, + 'FunctionNameTestTsEsModuleNested', + ); + + const payload = getSamplePayload(lambdaName); + const response = await callLambda(lambdaName, payload); + + expect(response.inputEvent).toEqual(payload); + expect(response.runningLocally).toEqual(!observableMode); + if (observableMode) { + await validateLocalResponse(lambdaName, payload); + } + }); + + test('call Lambda - testJsCommonJsNested', async () => { + const lambdaName = await getSamFunctionName( + folder, + 'FunctionNameTestJsCommonJsNested', + ); + + const payload = getSamplePayload(lambdaName); + const response = await callLambda(lambdaName, payload); + + expect(response.runningLocally).toEqual(!observableMode); + expect(response.inputEvent).toEqual(payload); + if (observableMode) { + await validateLocalResponse(lambdaName, payload); + } + }); + + test('remove infra', async () => { + if (process.env.CI === 'true' || process.env.RUN_TEST_FROM_CLI === 'true') { + await removeInfra(lldProcess, folder, ['--config-env=test']); + const lambdaName = await getSamFunctionName( + folder, + 'FunctionNameTestTsCommonJs', + ); + await expectInfraRemoved(lambdaName); + } + }); +}); + +export async function getSamFunctionName(folder: string, functionName: string) { + const outputs = JSON.parse( + await fs.readFile(`${folder}/sam-outputs.json`, 'utf-8'), + ); + const lambdaName = outputs.find( + (o: any) => o.OutputKey === functionName, + )?.OutputValue; + return lambdaName; +} diff --git a/test/sam-nested/.gitignore b/test/sam-nested/.gitignore new file mode 100644 index 00000000..67fb2695 --- /dev/null +++ b/test/sam-nested/.gitignore @@ -0,0 +1,2 @@ +.aws-sam +sam-outputs.json \ No newline at end of file diff --git a/test/sam-nested/nested-nested-stack.yaml b/test/sam-nested/nested-nested-stack.yaml new file mode 100644 index 00000000..000797a4 --- /dev/null +++ b/test/sam-nested/nested-nested-stack.yaml @@ -0,0 +1,75 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: >- + Nested Nested Stack for sam-basic + +Transform: + - AWS::Serverless-2016-10-31 + +Resources: + testTsCommonJs: + Type: AWS::Serverless::Function + Properties: + CodeUri: services/testTsCommonJs/ + Handler: lambda.lambdaHandler + Runtime: nodejs22.x + Architectures: + - x86_64 + MemorySize: 128 + Timeout: 10 + Policies: + - AWSLambdaBasicExecutionRole + Metadata: + BuildMethod: esbuild + BuildProperties: + Format: cjs + Minify: true + Target: 'es2020' + Sourcemap: true + EntryPoints: + - lambda.ts + + testTsEsModule: + Type: AWS::Serverless::Function + Properties: + CodeUri: services/testTsEsModule/ + Handler: lambda.lambdaHandler + Runtime: nodejs22.x + Architectures: + - x86_64 + MemorySize: 128 + Timeout: 10 + Policies: + - AWSLambdaBasicExecutionRole + Metadata: + BuildMethod: esbuild + BuildProperties: + Format: esm + OutExtension: + - .js=.mjs + Minify: true + Target: 'es2020' + Sourcemap: true + Banner: + - js=import { createRequire } from 'module'; const require = createRequire(import.meta.url); + EntryPoints: + - lambda.ts + + testJsCommonJs: + Type: AWS::Serverless::Function + Properties: + Handler: services/testJsCommonJs/lambda.lambdaHandler + Runtime: nodejs22.x + Architectures: + - x86_64 + MemorySize: 128 + Timeout: 10 + Policies: + - AWSLambdaBasicExecutionRole + +Outputs: + FunctionNameTestTsCommonJsNested: + Value: !Ref testTsCommonJs + FunctionNameTestTsEsModuleNested: + Value: !Ref testTsEsModule + FunctionNameTestJsCommonJsNested: + Value: !Ref testJsCommonJs diff --git a/test/sam-nested/nested-stack.yaml b/test/sam-nested/nested-stack.yaml new file mode 100644 index 00000000..4ff546fb --- /dev/null +++ b/test/sam-nested/nested-stack.yaml @@ -0,0 +1,89 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: >- + Nested Stack for sam-basic + +Transform: + - AWS::Serverless-2016-10-31 + +Resources: + # Using AWS::CloudFormation::Stack instead of AWS::Serverless::Application + # to test nested stack support with native CloudFormation resource type + NestedNestedStack: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: nested-nested-stack.yaml + + testTsCommonJs: + Type: AWS::Serverless::Function + Properties: + CodeUri: services/testTsCommonJs/ + Handler: lambda.lambdaHandler + Runtime: nodejs22.x + Architectures: + - x86_64 + MemorySize: 128 + Timeout: 10 + Policies: + - AWSLambdaBasicExecutionRole + Metadata: + BuildMethod: esbuild + BuildProperties: + Format: cjs + Minify: true + Target: 'es2020' + Sourcemap: true + EntryPoints: + - lambda.ts + + testTsEsModule: + Type: AWS::Serverless::Function + Properties: + CodeUri: services/testTsEsModule/ + Handler: lambda.lambdaHandler + Runtime: nodejs22.x + Architectures: + - x86_64 + MemorySize: 128 + Timeout: 10 + Policies: + - AWSLambdaBasicExecutionRole + Metadata: + BuildMethod: esbuild + BuildProperties: + Format: esm + OutExtension: + - .js=.mjs + Minify: true + Target: 'es2020' + Sourcemap: true + Banner: + - js=import { createRequire } from 'module'; const require = createRequire(import.meta.url); + EntryPoints: + - lambda.ts + + testJsCommonJs: + Type: AWS::Serverless::Function + Properties: + Handler: services/testJsCommonJs/lambda.lambdaHandler + Runtime: nodejs22.x + Architectures: + - x86_64 + MemorySize: 128 + Timeout: 10 + Policies: + - AWSLambdaBasicExecutionRole + +Outputs: + FunctionNameTestTsCommonJs: + Value: !Ref testTsCommonJs + FunctionNameTestTsEsModule: + Value: !Ref testTsEsModule + FunctionNameTestJsCommonJs: + Value: !Ref testJsCommonJs + # Pass through outputs from nested-nested stack + FunctionNameTestTsCommonJsNested: + Value: !GetAtt NestedNestedStack.Outputs.FunctionNameTestTsCommonJsNested + FunctionNameTestTsEsModuleNested: + Value: !GetAtt NestedNestedStack.Outputs.FunctionNameTestTsEsModuleNested + FunctionNameTestJsCommonJsNested: + Value: !GetAtt NestedNestedStack.Outputs.FunctionNameTestJsCommonJsNested diff --git a/test/sam-nested/package.json b/test/sam-nested/package.json new file mode 100755 index 00000000..02e5c3d4 --- /dev/null +++ b/test/sam-nested/package.json @@ -0,0 +1,18 @@ +{ + "name": "sam-nested", + "version": "0.0.1", + "private": true, + "scripts": { + "build": "sam build", + "deploy": "npm run build && sam deploy --config-env test --no-confirm-changeset --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND && npm run export_outputs", + "export_outputs": "aws cloudformation describe-stacks --stack-name lld-test-sam-nested --output json --query Stacks[0].Outputs > sam-outputs.json", + "destroy": "sam delete --config-env test --no-prompts" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.5", + "@types/aws-lambda": "^8.10.159" + }, + "dependencies": { + "@aws-sdk/client-sts": "^3.936.0" + } +} diff --git a/test/sam-nested/samconfig.toml b/test/sam-nested/samconfig.toml new file mode 100644 index 00000000..1683fad8 --- /dev/null +++ b/test/sam-nested/samconfig.toml @@ -0,0 +1,59 @@ +# More information about the configuration file can be found here: +# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html +version = 0.1 + +[default] +[default.global.parameters] +stack_name = "lld-sam-nested" + +[default.build.parameters] +cached = true +parallel = true + +[default.validate.parameters] +lint = true + +[default.deploy.parameters] +capabilities = "CAPABILITY_IAM" +confirm_changeset = true +resolve_s3 = true + +[default.package.parameters] +resolve_s3 = true + +[default.sync.parameters] +watch = true + +[default.local_start_api.parameters] +warm_containers = "EAGER" + +[default.local_start_lambda.parameters] +warm_containers = "EAGER" + +[test] +[test.global.parameters] +stack_name = "lld-test-sam-nested" + +[test.build.parameters] +cached = true +parallel = true + +[test.validate.parameters] +lint = true + +[test.deploy.parameters] +capabilities = "CAPABILITY_IAM" +confirm_changeset = true +resolve_s3 = true + +[test.package.parameters] +resolve_s3 = true + +[test.sync.parameters] +watch = true + +[test.local_start_api.parameters] +warm_containers = "EAGER" + +[test.local_start_lambda.parameters] +warm_containers = "EAGER" \ No newline at end of file diff --git a/test/sam-nested/services/testJsCommonJs/lambda.js b/test/sam-nested/services/testJsCommonJs/lambda.js new file mode 100755 index 00000000..4e37462d --- /dev/null +++ b/test/sam-nested/services/testJsCommonJs/lambda.js @@ -0,0 +1,40 @@ +const { STSClient, GetCallerIdentityCommand } = require('@aws-sdk/client-sts'); + +const stsClient = new STSClient({}); + +const lambdaHandler = async (event, context) => { + // Check context + const remainingTime = context.getRemainingTimeInMillis(); + if (remainingTime === undefined) { + throw new Error('Remaining time is undefined'); + } + + // check if SDK works + const command = new GetCallerIdentityCommand({}); + const identity = await stsClient.send(command); + + const response = { + inputEvent: event, + accountId: identity.Account, + runningLocally: process.env.IS_LOCAL === 'true', + }; + + if (process.env.IS_LOCAL === 'true') { + const fs = require('fs'); + const path = require('path'); + const filePath = path.join( + '..', + 'local_lambda_responses', + `${context.functionName}.json`, + ); + + fs.writeFileSync(filePath, JSON.stringify(response, null, 2)); + } + + return response; +}; + +// Export the lambda handler if needed, e.g., for unit testing +module.exports = { + lambdaHandler, +}; diff --git a/test/sam-nested/services/testJsCommonJs/package.json b/test/sam-nested/services/testJsCommonJs/package.json new file mode 100644 index 00000000..34ea9f57 --- /dev/null +++ b/test/sam-nested/services/testJsCommonJs/package.json @@ -0,0 +1,8 @@ +{ + "name": "sam-basic-test-js-commonjs", + "version": "1.0.0", + "type": "commonjs", + "dependencies": { + "@aws-sdk/client-sts": "^3.936.0" + } +} diff --git a/test/sam-nested/services/testJsEsModule/lambda.js b/test/sam-nested/services/testJsEsModule/lambda.js new file mode 100755 index 00000000..e3508208 --- /dev/null +++ b/test/sam-nested/services/testJsEsModule/lambda.js @@ -0,0 +1,35 @@ +import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'; + +const stsClient = new STSClient({}); + +export const lambdaHandler = async (event, context) => { + // check context + const remainingTime = context.getRemainingTimeInMillis(); + if (remainingTime === undefined) { + throw new Error('Remaining time is undefined'); + } + + // check SDK works + const command = new GetCallerIdentityCommand({}); + const identity = await stsClient.send(command); + + const response = { + inputEvent: event, + accountId: identity.Account, + runningLocally: process.env.IS_LOCAL === 'true', + }; + + if (process.env.IS_LOCAL === 'true') { + const fs = await import('fs'); + const path = await import('path'); + const filePath = path.join( + '..', + 'local_lambda_responses', + `${context.functionName}.json`, + ); + + fs.writeFileSync(filePath, JSON.stringify(response, null, 2)); + } + + return response; +}; diff --git a/test/sam-nested/services/testJsEsModule/package.json b/test/sam-nested/services/testJsEsModule/package.json new file mode 100644 index 00000000..c85c91a0 --- /dev/null +++ b/test/sam-nested/services/testJsEsModule/package.json @@ -0,0 +1,8 @@ +{ + "name": "sam-basic-test-js-esmodule", + "version": "1.0.0", + "type": "module", + "dependencies": { + "@aws-sdk/client-sts": "^3.936.0" + } +} diff --git a/test/sam-nested/services/testTsCommonJs/lambda.ts b/test/sam-nested/services/testTsCommonJs/lambda.ts new file mode 100755 index 00000000..70bd2e6e --- /dev/null +++ b/test/sam-nested/services/testTsCommonJs/lambda.ts @@ -0,0 +1,36 @@ +import { Handler } from 'aws-lambda'; +import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'; + +const stsClient = new STSClient({}); + +export const lambdaHandler: Handler = async (event, context) => { + // check context + const remainingTime = context.getRemainingTimeInMillis(); + if (remainingTime === undefined) { + throw new Error('Remaining time is undefined'); + } + + // check SDK works + const command = new GetCallerIdentityCommand({}); + const identity = await stsClient.send(command); + + const response = { + inputEvent: event, + accountId: identity.Account, + runningLocally: process.env.IS_LOCAL === 'true', + }; + + if (process.env.IS_LOCAL === 'true') { + const fs = await import('fs'); + const path = await import('path'); + const filePath = path.join( + '..', + 'local_lambda_responses', + `${context.functionName}.json`, + ); + + fs.writeFileSync(filePath, JSON.stringify(response, null, 2)); + } + + return response; +}; diff --git a/test/sam-nested/services/testTsCommonJs/package.json b/test/sam-nested/services/testTsCommonJs/package.json new file mode 100644 index 00000000..65c4c449 --- /dev/null +++ b/test/sam-nested/services/testTsCommonJs/package.json @@ -0,0 +1,11 @@ +{ + "name": "sam-basic-test-ts-commonjs", + "version": "1.0.0", + "type": "commonjs", + "devDependencies": { + "@tsconfig/node22": "^22.0.5" + }, + "dependencies": { + "@aws-sdk/client-sts": "^3.936.0" + } +} diff --git a/test/sam-nested/services/testTsCommonJs/tsconfig.json b/test/sam-nested/services/testTsCommonJs/tsconfig.json new file mode 100755 index 00000000..851b3d3b --- /dev/null +++ b/test/sam-nested/services/testTsCommonJs/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "moduleResolution": "node", + "module": "CommonJS" + } +} diff --git a/test/sam-nested/services/testTsEsModule/lambda.ts b/test/sam-nested/services/testTsEsModule/lambda.ts new file mode 100755 index 00000000..70bd2e6e --- /dev/null +++ b/test/sam-nested/services/testTsEsModule/lambda.ts @@ -0,0 +1,36 @@ +import { Handler } from 'aws-lambda'; +import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'; + +const stsClient = new STSClient({}); + +export const lambdaHandler: Handler = async (event, context) => { + // check context + const remainingTime = context.getRemainingTimeInMillis(); + if (remainingTime === undefined) { + throw new Error('Remaining time is undefined'); + } + + // check SDK works + const command = new GetCallerIdentityCommand({}); + const identity = await stsClient.send(command); + + const response = { + inputEvent: event, + accountId: identity.Account, + runningLocally: process.env.IS_LOCAL === 'true', + }; + + if (process.env.IS_LOCAL === 'true') { + const fs = await import('fs'); + const path = await import('path'); + const filePath = path.join( + '..', + 'local_lambda_responses', + `${context.functionName}.json`, + ); + + fs.writeFileSync(filePath, JSON.stringify(response, null, 2)); + } + + return response; +}; diff --git a/test/sam-nested/services/testTsEsModule/package.json b/test/sam-nested/services/testTsEsModule/package.json new file mode 100644 index 00000000..b598ed99 --- /dev/null +++ b/test/sam-nested/services/testTsEsModule/package.json @@ -0,0 +1,11 @@ +{ + "name": "sam-basic-test-ts-esmodule", + "version": "1.0.0", + "type": "module", + "devDependencies": { + "@tsconfig/node22": "^22.0.5" + }, + "dependencies": { + "@aws-sdk/client-sts": "^3.936.0" + } +} diff --git a/test/sam-nested/services/testTsEsModule/tsconfig.json b/test/sam-nested/services/testTsEsModule/tsconfig.json new file mode 100755 index 00000000..a1902a03 --- /dev/null +++ b/test/sam-nested/services/testTsEsModule/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "module": "esnext", + "moduleResolution": "node" + } +} diff --git a/test/sam-nested/template.yaml b/test/sam-nested/template.yaml new file mode 100755 index 00000000..305dafbd --- /dev/null +++ b/test/sam-nested/template.yaml @@ -0,0 +1,108 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: >- + sam-basic + +Transform: + - AWS::Serverless-2016-10-31 + +Resources: + NestedStack: + Type: AWS::Serverless::Application + Properties: + Location: nested-stack.yaml + + testTsCommonJs: + Type: AWS::Serverless::Function + Properties: + CodeUri: services/testTsCommonJs/ + Handler: lambda.lambdaHandler + Runtime: nodejs22.x + Architectures: + - x86_64 + MemorySize: 128 + Timeout: 10 + Policies: + - AWSLambdaBasicExecutionRole + Metadata: + BuildMethod: esbuild + BuildProperties: + Format: cjs + Minify: true + Target: 'es2020' + Sourcemap: true + EntryPoints: + - lambda.ts + + testTsEsModule: + Type: AWS::Serverless::Function + Properties: + CodeUri: services/testTsEsModule/ + Handler: lambda.lambdaHandler + Runtime: nodejs22.x + Architectures: + - x86_64 + MemorySize: 128 + Timeout: 10 + Policies: + - AWSLambdaBasicExecutionRole + Metadata: + BuildMethod: esbuild + BuildProperties: + Format: esm + OutExtension: + - .js=.mjs + Minify: true + Target: 'es2020' + Sourcemap: true + Banner: + - js=import { createRequire } from 'module'; const require = createRequire(import.meta.url); + EntryPoints: + - lambda.ts + + testJsCommonJs: + Type: AWS::Serverless::Function + Properties: + Handler: services/testJsCommonJs/lambda.lambdaHandler + Runtime: nodejs22.x + Architectures: + - x86_64 + MemorySize: 128 + Timeout: 10 + Policies: + - AWSLambdaBasicExecutionRole + + testJsEsModule: + Type: AWS::Serverless::Function + Properties: + Handler: services/testJsEsModule/lambda.lambdaHandler + Runtime: nodejs22.x + Architectures: + - x86_64 + MemorySize: 128 + Timeout: 10 + Policies: + - AWSLambdaBasicExecutionRole + +Outputs: + FunctionNameTestJsCommonJs: + Value: !Ref testJsCommonJs + FunctionNameTestJsEsModule: + Value: !Ref testJsEsModule + FunctionNameTestTsCommonJs: + Value: !Ref testTsCommonJs + FunctionNameTestTsEsModule: + Value: !Ref testTsEsModule + # Outputs from nested stack + FunctionNameTestTsCommonJsFromNestedStack: + Value: !GetAtt NestedStack.Outputs.FunctionNameTestTsCommonJs + FunctionNameTestTsEsModuleFromNestedStack: + Value: !GetAtt NestedStack.Outputs.FunctionNameTestTsEsModule + FunctionNameTestJsCommonJsFromNestedStack: + Value: !GetAtt NestedStack.Outputs.FunctionNameTestJsCommonJs + # Outputs from nested-nested stack (via nested stack) + FunctionNameTestTsCommonJsNested: + Value: !GetAtt NestedStack.Outputs.FunctionNameTestTsCommonJsNested + FunctionNameTestTsEsModuleNested: + Value: !GetAtt NestedStack.Outputs.FunctionNameTestTsEsModuleNested + FunctionNameTestJsCommonJsNested: + Value: !GetAtt NestedStack.Outputs.FunctionNameTestJsCommonJsNested