Skip to content

Commit 84a5ff8

Browse files
dreamorosiam29d
andauthored
test(maintenance): add ESM output to e2e test (#2370)
* test(maintenance): add esm output to e2e tests * test: switch 50% tests to esm * test: switch 50% tests to esm * test: switch 50% tests to esm * test: refactor layers test to deploy cjs & esm * chore: marked @AWS-SDK as external * chore: remove redundant boolean literals * chore: add max-parallel to avoid timeouts * chore: adjust max-parallel * chore: aws-sdk workaround * chore: override nodejs16 to bundle aws-sdk when working with esm * chore: exclude ESM on Node.js 16 from layer tests * docs: add callout about ESM & Layers in docs --------- Co-authored-by: Alexander Schueren <[email protected]>
1 parent 21ecc4f commit 84a5ff8

15 files changed

+150
-70
lines changed

.github/workflows/run-e2e-tests.yml

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jobs:
2121
id-token: write # needed to interact with GitHub's OIDC Token endpoint.
2222
contents: read
2323
strategy:
24+
max-parallel: 30
2425
matrix:
2526
package:
2627
[

docs/index.md

+2
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,8 @@ You can use Powertools for AWS Lambda (TypeScript) by installing it with your fa
254254

255255
[Lambda Layer](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html){target="_blank"} is a `.zip` file archive that can contain additional code, pre-packaged dependencies, data, or configuration files. We compile and optimize [all dependencies](#install) to achieve an optimal build.
256256

257+
You can use the Lambda Layer both with CommonJS and ESM (ECMAScript modules) for Node.js 18.x and newer runtimes. **If you are using the managed Node.js 16.x runtime and cannot upgrade, you should use the CommonJS version only**.
258+
257259
??? note "Click to expand and copy any regional Lambda Layer ARN"
258260
| Region | Layer ARN |
259261
| ---------------- | ------------------------------------------------------------------------------------------------------------- |

layers/src/layer-publisher-stack.ts

-2
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,7 @@ export class LayerPublisherStack extends Stack {
100100
'node_modules/@aws-lambda-powertools/*/lib/**/*.d.ts',
101101
'node_modules/@aws-lambda-powertools/*/lib/**/*.d.ts.map',
102102
'node_modules/@aws-sdk/*/dist-types',
103-
'node_modules/@aws-sdk/*/dist-es',
104103
'node_modules/@smithy/*/dist-types',
105-
'node_modules/@smithy/*/dist-es',
106104
'node_modules/@smithy/**/README.md ',
107105
'node_modules/@aws-sdk/**/README.md ',
108106
];

layers/tests/e2e/layerPublisher.test.ts

+81-62
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,52 @@ import {
1111
TestInvocationLogs,
1212
invokeFunctionOnce,
1313
generateTestUniqueName,
14+
getRuntimeKey,
1415
} from '@aws-lambda-powertools/testing-utils';
1516
import { TestNodejsFunction } from '@aws-lambda-powertools/testing-utils/resources/lambda';
1617
import {
1718
RESOURCE_NAME_PREFIX,
1819
SETUP_TIMEOUT,
1920
TEARDOWN_TIMEOUT,
20-
TEST_CASE_TIMEOUT,
2121
} from './constants';
2222
import { join } from 'node:path';
2323
import packageJson from '../../package.json';
2424

2525
jest.spyOn(console, 'log').mockImplementation();
2626

27+
// eslint-disable-next-line func-style -- type assertions can't be arrow functions
28+
function assertLogs(
29+
logs: TestInvocationLogs | undefined
30+
): asserts logs is TestInvocationLogs {
31+
if (!logs) {
32+
throw new Error('Function logs are not available');
33+
}
34+
}
35+
2736
/**
2837
* This test has two stacks:
2938
* 1. LayerPublisherStack - publishes a layer version using the LayerPublisher construct and containing the Powertools utilities from the repo
30-
* 2. TestStack - uses the layer published in the first stack and contains a lambda function that uses the Powertools utilities from the layer
39+
* 2. TestStack - uses the layer published in the first stack and contains two lambda functions that use the Powertools utilities from the layer
3140
*
3241
* The lambda function is invoked once and the logs are collected. The goal of the test is to verify that the layer creation and usage works as expected.
3342
*/
34-
describe(`Layers E2E tests, publisher stack`, () => {
43+
describe(`Layers E2E tests`, () => {
3544
const testStack = new TestStack({
3645
stackNameProps: {
3746
stackNamePrefix: RESOURCE_NAME_PREFIX,
3847
testName: 'functionStack',
3948
},
4049
});
4150

42-
let invocationLogs: TestInvocationLogs;
51+
/**
52+
* Node.js 16.x does not support importing ESM modules from Lambda Layers reliably.
53+
*
54+
* The feature is available in Node.js 18.x and later.
55+
* @see https://aws.amazon.com/blogs/compute/node-js-18-x-runtime-now-available-in-aws-lambda/
56+
*/
57+
const cases = getRuntimeKey() === 'nodejs16x' ? ['CJS'] : ['CJS', 'ESM'];
58+
const invocationLogsMap: Map<(typeof cases)[number], TestInvocationLogs> =
59+
new Map();
4360

4461
const ssmParameterLayerName = generateTestUniqueName({
4562
testPrefix: `${RESOURCE_NAME_PREFIX}`,
@@ -75,76 +92,83 @@ describe(`Layers E2E tests, publisher stack`, () => {
7592
});
7693

7794
beforeAll(async () => {
95+
// Deploy the stack that publishes the layer
7896
await testLayerStack.deploy();
7997

98+
// Import the layer version from the stack outputs into the test stack
8099
const layerVersion = LayerVersion.fromLayerVersionArn(
81100
testStack.stack,
82101
'LayerVersionArnReference',
83102
testLayerStack.findAndGetStackOutputValue('LatestLayerArn')
84103
);
85-
new TestNodejsFunction(
86-
testStack,
87-
{
88-
entry: lambdaFunctionCodeFilePath,
89-
environment: {
90-
LAYERS_PATH: '/opt/nodejs/node_modules',
91-
POWERTOOLS_PACKAGE_VERSION: powerToolsPackageVersion,
92-
POWERTOOLS_SERVICE_NAME: 'LayerPublisherStack',
93-
},
94-
bundling: {
95-
externalModules: [
96-
'@aws-lambda-powertools/commons',
97-
'@aws-lambda-powertools/logger',
98-
'@aws-lambda-powertools/metrics',
99-
'@aws-lambda-powertools/tracer',
100-
'@aws-lambda-powertools/parameter',
101-
'@aws-lambda-powertools/idempotency',
102-
'@aws-lambda-powertools/batch',
103-
],
104+
105+
// Add a lambda function for each output format to the test stack
106+
cases.forEach((outputFormat) => {
107+
new TestNodejsFunction(
108+
testStack,
109+
{
110+
entry: lambdaFunctionCodeFilePath,
111+
environment: {
112+
LAYERS_PATH: '/opt/nodejs/node_modules',
113+
POWERTOOLS_PACKAGE_VERSION: powerToolsPackageVersion,
114+
POWERTOOLS_SERVICE_NAME: 'LayerPublisherStack',
115+
},
116+
bundling: {
117+
externalModules: [
118+
'@aws-lambda-powertools/*',
119+
'@aws-sdk/*',
120+
'aws-xray-sdk-node',
121+
],
122+
},
123+
layers: [layerVersion],
104124
},
105-
layers: [layerVersion],
106-
},
107-
{
108-
nameSuffix: 'testFn',
109-
}
110-
);
125+
{
126+
nameSuffix: `test${outputFormat}Fn`,
127+
...(outputFormat === 'ESM' && { outputFormat: 'ESM' }),
128+
}
129+
);
130+
});
111131

132+
// Deploy the test stack
112133
await testStack.deploy();
113134

114-
const functionName = testStack.findAndGetStackOutputValue('testFn');
115-
116-
invocationLogs = await invokeFunctionOnce({
117-
functionName,
118-
});
135+
// Invoke the lambda function once for each output format and collect the logs
136+
for await (const outputFormat of cases) {
137+
invocationLogsMap.set(
138+
outputFormat,
139+
await invokeFunctionOnce({
140+
functionName: testStack.findAndGetStackOutputValue(
141+
`test${outputFormat}Fn`
142+
),
143+
})
144+
);
145+
}
119146
}, SETUP_TIMEOUT);
120147

121-
describe('package version and path check', () => {
122-
it(
123-
'should have no errors in the logs, which indicates the pacakges version matches the expected one',
124-
() => {
148+
describe.each(cases)(
149+
'utilities tests for %s output format',
150+
(outputFormat) => {
151+
let invocationLogs: TestInvocationLogs;
152+
beforeAll(() => {
153+
const maybeInvocationLogs = invocationLogsMap.get(outputFormat);
154+
assertLogs(maybeInvocationLogs);
155+
invocationLogs = maybeInvocationLogs;
156+
});
157+
158+
it('should have no errors in the logs, which indicates the pacakges version matches the expected one', () => {
125159
const logs = invocationLogs.getFunctionLogs('ERROR');
126160

127161
expect(logs.length).toBe(0);
128-
},
129-
TEST_CASE_TIMEOUT
130-
);
131-
});
162+
});
132163

133-
describe('utilities usage', () => {
134-
it(
135-
'should have one warning related to missing Metrics namespace',
136-
() => {
164+
it('should have one warning related to missing Metrics namespace', () => {
137165
const logs = invocationLogs.getFunctionLogs('WARN');
138166

139167
expect(logs.length).toBe(1);
140168
expect(logs[0]).toContain('Namespace should be defined, default used');
141-
},
142-
TEST_CASE_TIMEOUT
143-
);
169+
});
144170

145-
it(
146-
'should have one info log related to coldstart metric',
147-
() => {
171+
it('should have one info log related to coldstart metric', () => {
148172
const logs = invocationLogs.getFunctionLogs();
149173
const emfLogEntry = logs.find((log) =>
150174
log.match(
@@ -153,13 +177,9 @@ describe(`Layers E2E tests, publisher stack`, () => {
153177
);
154178

155179
expect(emfLogEntry).toBeDefined();
156-
},
157-
TEST_CASE_TIMEOUT
158-
);
180+
});
159181

160-
it(
161-
'should have one debug log with tracer subsegment info',
162-
() => {
182+
it('should have one debug log with tracer subsegment info', () => {
163183
const logs = invocationLogs.getFunctionLogs('DEBUG');
164184

165185
expect(logs.length).toBe(1);
@@ -182,10 +202,9 @@ describe(`Layers E2E tests, publisher stack`, () => {
182202
trace_id: traceIdFromLog,
183203
})
184204
);
185-
},
186-
TEST_CASE_TIMEOUT
187-
);
188-
});
205+
});
206+
}
207+
);
189208

190209
afterAll(async () => {
191210
if (!process.env.DISABLE_TEARDOWN) {

packages/idempotency/tests/e2e/idempotentDecorator.test.ts

+5
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ describe('Idempotency e2e test decorator, default settings', () => {
6363
},
6464
{
6565
nameSuffix: 'defaultParallel',
66+
outputFormat: 'ESM',
6667
}
6768
);
6869

@@ -79,6 +80,7 @@ describe('Idempotency e2e test decorator, default settings', () => {
7980
},
8081
{
8182
nameSuffix: 'timeout',
83+
outputFormat: 'ESM',
8284
}
8385
);
8486

@@ -95,6 +97,7 @@ describe('Idempotency e2e test decorator, default settings', () => {
9597
},
9698
{
9799
nameSuffix: 'expired',
100+
outputFormat: 'ESM',
98101
}
99102
);
100103

@@ -110,6 +113,7 @@ describe('Idempotency e2e test decorator, default settings', () => {
110113
},
111114
{
112115
nameSuffix: 'dataIndex',
116+
outputFormat: 'ESM',
113117
}
114118
);
115119

@@ -131,6 +135,7 @@ describe('Idempotency e2e test decorator, default settings', () => {
131135
},
132136
{
133137
nameSuffix: 'customConfig',
138+
outputFormat: 'ESM',
134139
}
135140
);
136141

packages/logger/tests/e2e/basicFeatures.middy.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ describe(`Logger E2E tests, basic functionalities middy usage`, () => {
4848
{
4949
logGroupOutputKey: STACK_OUTPUT_LOG_GROUP,
5050
nameSuffix: 'BasicFeatures',
51+
outputFormat: 'ESM',
5152
}
5253
);
5354

packages/logger/tests/e2e/sampleRate.decorator.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ describe(`Logger E2E tests, sample rate and injectLambdaContext()`, () => {
5252
{
5353
logGroupOutputKey: STACK_OUTPUT_LOG_GROUP,
5454
nameSuffix: 'BasicFeatures',
55+
outputFormat: 'ESM',
5556
}
5657
);
5758

packages/metrics/tests/e2e/basicFeatures.decorators.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ describe(`Metrics E2E tests, basic features decorator usage`, () => {
5050
},
5151
{
5252
nameSuffix: 'BasicFeatures',
53+
outputFormat: 'ESM',
5354
}
5455
);
5556

packages/parameters/tests/e2e/appConfigProvider.class.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ describe(`Parameters E2E tests, AppConfig provider`, () => {
120120
},
121121
{
122122
nameSuffix: 'appConfigProvider',
123+
outputFormat: 'ESM',
123124
}
124125
);
125126

packages/parameters/tests/e2e/secretsProvider.class.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ describe(`Parameters E2E tests, Secrets Manager provider`, () => {
6060
},
6161
{
6262
nameSuffix: 'secretsProvider',
63+
outputFormat: 'ESM',
6364
}
6465
);
6566

packages/testing/src/resources/TestNodejsFunction.ts

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { CfnOutput, Duration } from 'aws-cdk-lib';
22
import { Tracing } from 'aws-cdk-lib/aws-lambda';
3-
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
3+
import { NodejsFunction, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs';
44
import { RetentionDays } from 'aws-cdk-lib/aws-logs';
55
import { randomUUID } from 'node:crypto';
66
import { TEST_RUNTIMES, TEST_ARCHITECTURES } from '../constants.js';
@@ -23,11 +23,24 @@ class TestNodejsFunction extends NodejsFunction {
2323
props: TestNodejsFunctionProps,
2424
extraProps: ExtraTestProps
2525
) {
26+
const isESM = extraProps.outputFormat === 'ESM';
27+
const { bundling, ...restProps } = props;
28+
2629
super(stack.stack, `fn-${randomUUID().substring(0, 5)}`, {
2730
timeout: Duration.seconds(30),
28-
memorySize: 256,
31+
memorySize: 512,
2932
tracing: Tracing.ACTIVE,
30-
...props,
33+
bundling: {
34+
...bundling,
35+
minify: true,
36+
mainFields: isESM ? ['module', 'main'] : ['main', 'module'],
37+
sourceMap: false,
38+
format: isESM ? OutputFormat.ESM : OutputFormat.CJS,
39+
banner: isESM
40+
? `import { createRequire } from 'module';const require = createRequire(import.meta.url);`
41+
: '',
42+
},
43+
...restProps,
3144
functionName: concatenateResourceName({
3245
testName: stack.testName,
3346
resourceName: extraProps.nameSuffix,

packages/testing/src/types.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ interface ExtraTestProps {
1313
* Note that the maximum length of the name is 64 characters, so the suffix might be truncated.
1414
*/
1515
nameSuffix: string;
16+
/**
17+
* The output format of the bundled code.
18+
*
19+
* @default 'CJS'
20+
*/
21+
outputFormat?: 'CJS' | 'ESM';
1622
}
1723

1824
type TestDynamodbTableProps = Omit<
@@ -27,8 +33,13 @@ type TestDynamodbTableProps = Omit<
2733

2834
type TestNodejsFunctionProps = Omit<
2935
NodejsFunctionProps,
30-
'logRetention' | 'runtime' | 'functionName'
31-
>;
36+
'logRetention' | 'runtime' | 'functionName' | 'bundling'
37+
> & {
38+
bundling?: Omit<
39+
NodejsFunctionProps['bundling'],
40+
'minify' | 'mainFields' | 'sourceMap' | 'format' | 'banner'
41+
>;
42+
};
3243

3344
type InvokeTestFunctionOptions = {
3445
functionName: string;

0 commit comments

Comments
 (0)