Skip to content

Commit 1f0abd8

Browse files
authored
feat(dev-mode): match shim runtime to local Node.js version (#13362)
1 parent d0d9ee3 commit 1f0abd8

3 files changed

Lines changed: 118 additions & 24 deletions

File tree

packages/serverless/lib/plugins/aws/dev/index.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ if (__dirname.endsWith('dist')) {
2424
__dirname = path.join(__dirname, '../lib/plugins/aws/dev')
2525
}
2626

27+
const AWS_LAMBDA_SUPPORTED_NODE_RUNTIMES = [
28+
'nodejs18.x',
29+
'nodejs20.x',
30+
'nodejs22.x',
31+
'nodejs24.x',
32+
]
33+
2734
const MQTT_PAYLOAD_LIMIT = 125 * 1024 // 125 KB
2835
const PAYLOAD_LIMIT_EXCEEDED_INSTRUCTION =
2936
'Deploy and invoke the function to test with large responses.'
@@ -283,7 +290,7 @@ class AwsDev {
283290
* Updates the serverless service configuration with dev mode config needed for the shim to work. Specifically:
284291
* 1. Update all AWS Lambda functions' IAM roles to allow all IoT actions.
285292
* 2. Update all AWS Lambad function's handler to 'index.handler' as set in the shim
286-
* 3. Update all AWS Lambda functions' runtime to 'nodejs20.x' as expected by the shim
293+
* 3. Update all AWS Lambda functions' runtime to match the local Node.js runtime when supported by AWS Lambda
287294
* 4. Update all AWS Lambda functions' environment variables to include the IoT endpoint and a function identifier.
288295
*
289296
* This method also backs up the original IAM configuration and function configurations to allow for later restoration.
@@ -331,8 +338,19 @@ class AwsDev {
331338
const stageName = this.serverless.getProvider('aws').getStage()
332339
const localRuntimeVersion = process.version.split('.')[0].replace('v', '')
333340
const localRuntime = `nodejs${localRuntimeVersion}.x`
341+
const runtimeForShim = AWS_LAMBDA_SUPPORTED_NODE_RUNTIMES.includes(
342+
localRuntime,
343+
)
344+
? localRuntime
345+
: 'nodejs20.x'
334346
let atLeastOneRuntimeVersionMismatch = false
335347

348+
if (runtimeForShim !== localRuntime) {
349+
logger.warning(
350+
`Your local machine is using Node.js v${localRuntimeVersion}, which is not yet supported by AWS Lambda. Falling back to ${runtimeForShim} for dev mode deployment.`,
351+
)
352+
}
353+
336354
const allFunctions = this.serverless.service.getAllFunctions()
337355

338356
const notNodeFunction = allFunctions.find((functionName) => {
@@ -377,8 +395,7 @@ class AwsDev {
377395
return false
378396
})
379397

380-
// Warn the user if the local runtime version does not match event one function runtime version
381-
if (atLeastOneRuntimeVersionMismatch) {
398+
if (atLeastOneRuntimeVersionMismatch && runtimeForShim === localRuntime) {
382399
logger.warning(
383400
`Your local machine is using Node.js v${localRuntimeVersion}, while at least one of your functions is not. Ensure matching runtime versions for accurate testing.`,
384401
)
@@ -394,7 +411,7 @@ class AwsDev {
394411
functionConfig.originalHandler = functionConfig.handler
395412

396413
functionConfig.handler = 'index.handler'
397-
functionConfig.runtime = 'nodejs20.x'
414+
functionConfig.runtime = runtimeForShim
398415

399416
functionConfig.environment = functionConfig.environment || {}
400417

packages/serverless/lib/plugins/aws/dev/local-lambda/index.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -288,19 +288,21 @@ const fileExists = async (filePath) => {
288288
* - List of runtime versions that the wrapper supports
289289
* - List of file extensions that the wrapper supports
290290
*/
291+
const SUPPORTED_NODE_RUNTIMES = [
292+
'nodejs14.x',
293+
'nodejs16.x',
294+
'nodejs18.x',
295+
'nodejs20.x',
296+
'nodejs22.x',
297+
'nodejs24.x',
298+
]
299+
291300
const runtimeWrappers = [
292301
{
293302
command: 'node',
294303
arguments: [],
295304
path: path.join(__dirname, 'runtime-wrappers/node.js'),
296-
versions: [
297-
'nodejs14.x',
298-
'nodejs16.x',
299-
'nodejs18.x',
300-
'nodejs20.x',
301-
'nodejs22.x',
302-
'nodejs24.x',
303-
],
305+
versions: SUPPORTED_NODE_RUNTIMES,
304306
extensions: ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'],
305307
},
306308
]

packages/serverless/test/unit/lib/plugins/aws/dev.test.js

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
1-
import { jest, describe, it, expect, beforeEach } from '@jest/globals'
1+
import {
2+
jest,
3+
describe,
4+
it,
5+
expect,
6+
beforeEach,
7+
afterEach,
8+
} from '@jest/globals'
9+
10+
const mockLogger = {
11+
logoDevMode: jest.fn(),
12+
blankLine: jest.fn(),
13+
aside: jest.fn(),
14+
notice: jest.fn(),
15+
debug: jest.fn(),
16+
warning: jest.fn(),
17+
confirm: jest.fn(),
18+
success: jest.fn(),
19+
error: jest.fn(),
20+
}
221

322
jest.unstable_mockModule('@serverless/util', () => ({
423
log: {
5-
get: jest.fn(() => ({
6-
logoDevMode: jest.fn(),
7-
blankLine: jest.fn(),
8-
aside: jest.fn(),
9-
notice: jest.fn(),
10-
debug: jest.fn(),
11-
warning: jest.fn(),
12-
confirm: jest.fn(),
13-
success: jest.fn(),
14-
error: jest.fn(),
15-
})),
24+
get: jest.fn(() => mockLogger),
1625
error: jest.fn(),
1726
blankLine: jest.fn(),
1827
warning: jest.fn(),
@@ -43,6 +52,14 @@ jest.unstable_mockModule('@serverless/util', () => ({
4352
const { default: AwsDev } =
4453
await import('../../../../../lib/plugins/aws/dev/index.js')
4554

55+
const originalProcessVersion = process.version
56+
57+
const setProcessVersion = (version) => {
58+
Object.defineProperty(process, 'version', {
59+
configurable: true,
60+
value: version,
61+
})
62+
}
4663
const createServerless = () => {
4764
const provider = {
4865
getStage: jest.fn(),
@@ -53,8 +70,15 @@ const createServerless = () => {
5370
return {
5471
getProvider: jest.fn(() => provider),
5572
processedInput: { commands: [] },
73+
configurationInput: {},
5674
service: {
5775
provider: {},
76+
getServiceName: jest.fn(() => 'test-service'),
77+
getAllFunctions: jest.fn(() => ['hello']),
78+
getFunction: jest.fn(() => ({
79+
handler: 'handler.main',
80+
runtime: 'nodejs20.x',
81+
})),
5882
},
5983
}
6084
}
@@ -63,9 +87,15 @@ describe('AwsDev', () => {
6387
let awsDev
6488

6589
beforeEach(() => {
90+
jest.clearAllMocks()
91+
setProcessVersion(originalProcessVersion)
6692
awsDev = new AwsDev(createServerless(), {})
6793
})
6894

95+
afterEach(() => {
96+
setProcessVersion(originalProcessVersion)
97+
})
98+
6999
describe('#validateOnExitOption()', () => {
70100
it('should not throw when --on-exit is not provided', () => {
71101
expect(() => awsDev.validateOnExitOption()).not.toThrow()
@@ -91,4 +121,49 @@ describe('AwsDev', () => {
91121
}
92122
})
93123
})
124+
125+
describe('#update()', () => {
126+
it('should set runtime to local node runtime when it is supported by AWS Lambda', async () => {
127+
setProcessVersion('v22.1.0')
128+
awsDev.getIotEndpoint = jest.fn().mockResolvedValue('iot-endpoint')
129+
130+
const functionConfig = {
131+
handler: 'handler.main',
132+
runtime: 'nodejs20.x',
133+
}
134+
135+
awsDev.serverless.service.getFunction = jest.fn(() => functionConfig)
136+
awsDev.serverless.service.provider.iam = {}
137+
awsDev.serverless.getProvider().getStage.mockReturnValue('dev')
138+
139+
await awsDev.update()
140+
141+
expect(functionConfig.runtime).toBe('nodejs22.x')
142+
expect(mockLogger.warning).toHaveBeenCalledWith(
143+
'Your local machine is using Node.js v22, while at least one of your functions is not. Ensure matching runtime versions for accurate testing.',
144+
)
145+
})
146+
147+
it('should fall back to nodejs20.x when local node runtime is not supported by AWS Lambda', async () => {
148+
setProcessVersion('v26.0.0')
149+
awsDev.getIotEndpoint = jest.fn().mockResolvedValue('iot-endpoint')
150+
151+
const functionConfig = {
152+
handler: 'handler.main',
153+
runtime: 'nodejs20.x',
154+
}
155+
156+
awsDev.serverless.service.getFunction = jest.fn(() => functionConfig)
157+
awsDev.serverless.service.provider.iam = {}
158+
awsDev.serverless.getProvider().getStage.mockReturnValue('dev')
159+
160+
await awsDev.update()
161+
162+
expect(functionConfig.runtime).toBe('nodejs20.x')
163+
expect(mockLogger.warning).toHaveBeenCalledTimes(1)
164+
expect(mockLogger.warning).toHaveBeenCalledWith(
165+
'Your local machine is using Node.js v26, which is not yet supported by AWS Lambda. Falling back to nodejs20.x for dev mode deployment.',
166+
)
167+
})
168+
})
94169
})

0 commit comments

Comments
 (0)