serverless-offline 9.2.5 → 9.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/package.json +5 -6
- package/src/ServerlessOffline.js +9 -8
- package/src/config/supportedRuntimes.js +5 -5
- package/src/lambda/LambdaFunction.js +53 -22
- package/src/lambda/LambdaFunctionPool.js +6 -5
- package/src/lambda/handler-runner/HandlerRunner.js +8 -1
- package/src/lambda/handler-runner/in-process-runner/InProcessRunner.js +7 -22
- package/src/lambda/handler-runner/ruby-runner/RubyRunner.js +1 -5
- package/src/utils/index.js +4 -3
package/README.md
CHANGED
|
@@ -215,10 +215,10 @@ the Lambda handler process is running in a child process.
|
|
|
215
215
|
To use `Lambda.invoke` you need to set the lambda endpoint to the `serverless-offline` endpoint:
|
|
216
216
|
|
|
217
217
|
```js
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
import { env } from 'node:process'
|
|
219
|
+
import aws from 'aws-sdk'
|
|
220
220
|
|
|
221
|
-
const lambda = new Lambda({
|
|
221
|
+
const lambda = new aws.Lambda({
|
|
222
222
|
apiVersion: '2015-03-31',
|
|
223
223
|
// endpoint needs to be set only if it deviates from the default
|
|
224
224
|
endpoint: env.IS_OFFLINE
|
|
@@ -230,17 +230,17 @@ const lambda = new Lambda({
|
|
|
230
230
|
All your lambdas can then be invoked in a handler using
|
|
231
231
|
|
|
232
232
|
```js
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
import { Buffer } from 'node:buffer'
|
|
234
|
+
import aws from 'aws-sdk'
|
|
235
235
|
|
|
236
236
|
const { stringify } = JSON
|
|
237
237
|
|
|
238
|
-
const lambda = new Lambda({
|
|
238
|
+
const lambda = new aws.Lambda({
|
|
239
239
|
apiVersion: '2015-03-31',
|
|
240
240
|
endpoint: 'http://localhost:3002',
|
|
241
241
|
})
|
|
242
242
|
|
|
243
|
-
|
|
243
|
+
export async function handler() {
|
|
244
244
|
const clientContextData = stringify({ foo: 'foo' })
|
|
245
245
|
|
|
246
246
|
const params = {
|
|
@@ -623,9 +623,9 @@ Usage in order to send messages back to clients:
|
|
|
623
623
|
Or,
|
|
624
624
|
|
|
625
625
|
```js
|
|
626
|
-
|
|
626
|
+
import aws from 'aws-sdk'
|
|
627
627
|
|
|
628
|
-
const apiGatewayManagementApi = new ApiGatewayManagementApi({
|
|
628
|
+
const apiGatewayManagementApi = new aws.ApiGatewayManagementApi({
|
|
629
629
|
apiVersion: '2018-11-29',
|
|
630
630
|
endpoint: 'http://localhost:3001',
|
|
631
631
|
});
|
package/package.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dedicatedTo": "Blue, a great migrating bird.",
|
|
3
3
|
"name": "serverless-offline",
|
|
4
|
-
"version": "9.
|
|
4
|
+
"version": "9.3.1",
|
|
5
5
|
"description": "Emulate AWS λ and API Gateway locally when developing your Serverless project",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"main": "./src/index.js",
|
|
8
7
|
"exports": "./src/index.js",
|
|
9
8
|
"type": "module",
|
|
10
9
|
"scripts": {
|
|
@@ -189,18 +188,18 @@
|
|
|
189
188
|
"@hapi/h2o2": "^9.1.0",
|
|
190
189
|
"@hapi/hapi": "^20.2.2",
|
|
191
190
|
"@serverless/utils": "^6.7.0",
|
|
192
|
-
"aws-sdk": "^2.
|
|
191
|
+
"aws-sdk": "^2.1205.0",
|
|
193
192
|
"boxen": "^7.0.0",
|
|
194
193
|
"chalk": "^5.0.1",
|
|
195
194
|
"execa": "^6.1.0",
|
|
196
195
|
"fs-extra": "^10.1.0",
|
|
197
196
|
"java-invoke-local": "0.0.6",
|
|
198
197
|
"js-string-escape": "^1.0.1",
|
|
199
|
-
"jsonpath-plus": "^7.
|
|
198
|
+
"jsonpath-plus": "^7.1.0",
|
|
200
199
|
"jsonschema": "^1.4.1",
|
|
201
200
|
"jsonwebtoken": "^8.5.1",
|
|
202
201
|
"jszip": "^3.10.1",
|
|
203
|
-
"luxon": "^3.0.
|
|
202
|
+
"luxon": "^3.0.3",
|
|
204
203
|
"node-fetch": "^3.2.10",
|
|
205
204
|
"node-schedule": "^2.1.0",
|
|
206
205
|
"object.hasown": "^1.1.1",
|
|
@@ -211,7 +210,7 @@
|
|
|
211
210
|
},
|
|
212
211
|
"devDependencies": {
|
|
213
212
|
"archiver": "^5.3.1",
|
|
214
|
-
"eslint": "^8.
|
|
213
|
+
"eslint": "^8.23.0",
|
|
215
214
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
216
215
|
"eslint-config-prettier": "^8.5.0",
|
|
217
216
|
"eslint-plugin-import": "^2.25.4",
|
package/src/ServerlessOffline.js
CHANGED
|
@@ -83,6 +83,15 @@ export default class ServerlessOffline {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
await Promise.all(eventModules)
|
|
86
|
+
|
|
87
|
+
if (this.#options.useChildProcesses) {
|
|
88
|
+
log.notice()
|
|
89
|
+
log.warning(
|
|
90
|
+
orange(`'--useChildProcesses' is deprecated and will be removed in the next major version. Worker threads, the current default, should provide the same if not an even better developer experience.
|
|
91
|
+
If you are experiencing any issues please let us know: https://github.com/dherault/serverless-offline/issues`),
|
|
92
|
+
)
|
|
93
|
+
log.notice()
|
|
94
|
+
}
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
async #ready() {
|
|
@@ -225,14 +234,6 @@ export default class ServerlessOffline {
|
|
|
225
234
|
...this.#cliOptions,
|
|
226
235
|
}
|
|
227
236
|
|
|
228
|
-
if (this.#options.useChildProcesses) {
|
|
229
|
-
log.notice()
|
|
230
|
-
log.warning(
|
|
231
|
-
orange(`'--useChildProcesses' is deprecated and will be removed in the next major version. Worker threads, the current default, should provide the same if not an even better developer experience.
|
|
232
|
-
If you are experiencing any issues please let us know: https://github.com/dherault/serverless-offline/issues`),
|
|
233
|
-
)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
237
|
// Parse CORS options
|
|
237
238
|
this.#options.corsAllowHeaders = this.#options.corsAllowHeaders
|
|
238
239
|
.replace(/\s/g, '')
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html
|
|
3
3
|
|
|
4
4
|
// .NET CORE
|
|
5
|
-
export const supportedDotnetcore = new Set([
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
])
|
|
5
|
+
// export const supportedDotnetcore = new Set([
|
|
6
|
+
// 'dotnet6',
|
|
7
|
+
// 'dotnetcore3.1',
|
|
8
|
+
// ])
|
|
9
9
|
|
|
10
10
|
// GO
|
|
11
11
|
export const supportedGo = new Set(['go1.x'])
|
|
@@ -37,7 +37,7 @@ export const supportedRuby = new Set(['ruby2.7'])
|
|
|
37
37
|
// deprecated runtimes
|
|
38
38
|
// https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html
|
|
39
39
|
export const supportedRuntimes = new Set([
|
|
40
|
-
...supportedDotnetcore,
|
|
40
|
+
// ...supportedDotnetcore,
|
|
41
41
|
...supportedGo,
|
|
42
42
|
...supportedJava,
|
|
43
43
|
...supportedNodejs,
|
|
@@ -2,6 +2,7 @@ import { readFile, writeFile } from 'node:fs/promises'
|
|
|
2
2
|
import { dirname, join, resolve } from 'node:path'
|
|
3
3
|
import process from 'node:process'
|
|
4
4
|
import { performance } from 'node:perf_hooks'
|
|
5
|
+
import { promisify } from 'node:util'
|
|
5
6
|
import { log } from '@serverless/utils/log.js'
|
|
6
7
|
import { emptyDir, ensureDir, remove } from 'fs-extra'
|
|
7
8
|
import jszip from 'jszip'
|
|
@@ -18,6 +19,8 @@ import { createUniqueId } from '../utils/index.js'
|
|
|
18
19
|
const { ceil } = Math
|
|
19
20
|
const { entries, fromEntries } = Object
|
|
20
21
|
|
|
22
|
+
const setTimeoutPromise = promisify(setTimeout)
|
|
23
|
+
|
|
21
24
|
export default class LambdaFunction {
|
|
22
25
|
#artifact = null
|
|
23
26
|
|
|
@@ -37,6 +40,8 @@ export default class LambdaFunction {
|
|
|
37
40
|
|
|
38
41
|
#handler = null
|
|
39
42
|
|
|
43
|
+
#handlerRunDone = false
|
|
44
|
+
|
|
40
45
|
#handlerRunner = null
|
|
41
46
|
|
|
42
47
|
#idleTimeStarted = null
|
|
@@ -49,13 +54,15 @@ export default class LambdaFunction {
|
|
|
49
54
|
|
|
50
55
|
#memorySize = null
|
|
51
56
|
|
|
57
|
+
#noTimeout = null
|
|
58
|
+
|
|
52
59
|
#region = null
|
|
53
60
|
|
|
54
61
|
#runtime = null
|
|
55
62
|
|
|
56
|
-
#
|
|
63
|
+
#status = 'IDLE' // can be 'BUSY' or 'IDLE'
|
|
57
64
|
|
|
58
|
-
|
|
65
|
+
#timeout = null
|
|
59
66
|
|
|
60
67
|
constructor(functionKey, functionDefinition, serverless, options) {
|
|
61
68
|
const {
|
|
@@ -71,28 +78,28 @@ export default class LambdaFunction {
|
|
|
71
78
|
|
|
72
79
|
const { handler, name, package: functionPackage = {} } = functionDefinition
|
|
73
80
|
|
|
74
|
-
|
|
81
|
+
// this._executionTimeout = null
|
|
82
|
+
this.#functionKey = functionKey
|
|
83
|
+
this.#functionName = name
|
|
84
|
+
this.#handler = handler
|
|
85
|
+
|
|
86
|
+
this.#memorySize =
|
|
75
87
|
functionDefinition.memorySize ||
|
|
76
88
|
provider.memorySize ||
|
|
77
89
|
DEFAULT_LAMBDA_MEMORY_SIZE
|
|
78
90
|
|
|
79
|
-
|
|
91
|
+
this.#noTimeout = options.noTimeout
|
|
92
|
+
|
|
93
|
+
this.#region = provider.region
|
|
94
|
+
|
|
95
|
+
this.#runtime =
|
|
80
96
|
functionDefinition.runtime || provider.runtime || DEFAULT_LAMBDA_RUNTIME
|
|
81
97
|
|
|
82
|
-
|
|
98
|
+
this.#timeout =
|
|
83
99
|
(functionDefinition.timeout ||
|
|
84
100
|
provider.timeout ||
|
|
85
101
|
DEFAULT_LAMBDA_TIMEOUT) * 1000
|
|
86
102
|
|
|
87
|
-
// this._executionTimeout = null
|
|
88
|
-
this.#functionKey = functionKey
|
|
89
|
-
this.#functionName = name
|
|
90
|
-
this.#handler = handler
|
|
91
|
-
this.#memorySize = memorySize
|
|
92
|
-
this.#region = provider.region
|
|
93
|
-
this.#runtime = runtime
|
|
94
|
-
this.#timeout = timeout
|
|
95
|
-
|
|
96
103
|
this.#verifySupportedRuntime()
|
|
97
104
|
|
|
98
105
|
const env = {
|
|
@@ -141,17 +148,17 @@ export default class LambdaFunction {
|
|
|
141
148
|
handler,
|
|
142
149
|
layers: functionDefinition.layers || [],
|
|
143
150
|
provider,
|
|
144
|
-
runtime,
|
|
151
|
+
runtime: this.#runtime,
|
|
145
152
|
serverlessPath,
|
|
146
153
|
servicePackage: servicePackage.artifact
|
|
147
154
|
? resolve(servicepath, servicePackage.artifact)
|
|
148
155
|
: undefined,
|
|
149
156
|
servicePath: servicepath,
|
|
150
|
-
timeout,
|
|
157
|
+
timeout: this.#timeout,
|
|
151
158
|
}
|
|
152
159
|
|
|
153
160
|
this.#handlerRunner = new HandlerRunner(funOptions, options, env)
|
|
154
|
-
this.#lambdaContext = new LambdaContext(name, memorySize)
|
|
161
|
+
this.#lambdaContext = new LambdaContext(name, this.#memorySize)
|
|
155
162
|
}
|
|
156
163
|
|
|
157
164
|
#startExecutionTimer() {
|
|
@@ -259,16 +266,33 @@ export default class LambdaFunction {
|
|
|
259
266
|
this.#initialized = true
|
|
260
267
|
}
|
|
261
268
|
|
|
262
|
-
get
|
|
263
|
-
return
|
|
269
|
+
get idleTimeInMillis() {
|
|
270
|
+
return performance.now() - this.#idleTimeStarted
|
|
264
271
|
}
|
|
265
272
|
|
|
266
273
|
get functionName() {
|
|
267
274
|
return this.#functionName
|
|
268
275
|
}
|
|
269
276
|
|
|
277
|
+
get status() {
|
|
278
|
+
return this.#status
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async #timeoutAndTerminate() {
|
|
282
|
+
await setTimeoutPromise(this.#timeout)
|
|
283
|
+
|
|
284
|
+
// if the handler has finished before the timeout don't terminate
|
|
285
|
+
if (this.#handlerRunDone) {
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
await this.#handlerRunner.cleanup()
|
|
290
|
+
|
|
291
|
+
throw new Error('Lambda timeout.')
|
|
292
|
+
}
|
|
293
|
+
|
|
270
294
|
async runHandler() {
|
|
271
|
-
this
|
|
295
|
+
this.#status = 'BUSY'
|
|
272
296
|
|
|
273
297
|
if (!this.#initialized) {
|
|
274
298
|
await this.#initialize()
|
|
@@ -283,7 +307,14 @@ export default class LambdaFunction {
|
|
|
283
307
|
|
|
284
308
|
this.#startExecutionTimer()
|
|
285
309
|
|
|
286
|
-
|
|
310
|
+
this.#handlerRunDone = false
|
|
311
|
+
|
|
312
|
+
const result = await Promise.race([
|
|
313
|
+
this.#handlerRunner.run(this.#event, context),
|
|
314
|
+
...(this.#noTimeout ? [] : [this.#timeoutAndTerminate()]),
|
|
315
|
+
])
|
|
316
|
+
|
|
317
|
+
this.#handlerRunDone = true
|
|
287
318
|
|
|
288
319
|
this.#stopExecutionTimer()
|
|
289
320
|
|
|
@@ -298,7 +329,7 @@ export default class LambdaFunction {
|
|
|
298
329
|
)
|
|
299
330
|
}
|
|
300
331
|
|
|
301
|
-
this
|
|
332
|
+
this.#status = 'IDLE'
|
|
302
333
|
this.#startIdleTimer()
|
|
303
334
|
|
|
304
335
|
return result
|
|
@@ -20,19 +20,20 @@ export default class LambdaFunctionPool {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
#startCleanTimer() {
|
|
23
|
+
const functionCleanupIdleTimeInMillis =
|
|
24
|
+
this.#options.functionCleanupIdleTimeSeconds * 1000
|
|
25
|
+
|
|
23
26
|
// NOTE: don't use setInterval, as it would schedule always a new run,
|
|
24
27
|
// regardless of function processing time and e.g. user action (debugging)
|
|
25
28
|
this.#timerRef = setTimeout(() => {
|
|
26
29
|
// console.log('run cleanup')
|
|
27
30
|
this.#pool.forEach((lambdaFunctions) => {
|
|
28
31
|
lambdaFunctions.forEach((lambdaFunction) => {
|
|
29
|
-
const {
|
|
30
|
-
// console.log(idleTimeInMinutes, status)
|
|
32
|
+
const { idleTimeInMillis, status } = lambdaFunction
|
|
31
33
|
|
|
32
34
|
if (
|
|
33
35
|
status === 'IDLE' &&
|
|
34
|
-
|
|
35
|
-
this.#options.functionCleanupIdleTimeSeconds / 60
|
|
36
|
+
idleTimeInMillis >= functionCleanupIdleTimeInMillis
|
|
36
37
|
) {
|
|
37
38
|
// console.log(`removed Lambda Function ${lambdaFunction.functionName}`)
|
|
38
39
|
lambdaFunction.cleanup()
|
|
@@ -43,7 +44,7 @@ export default class LambdaFunctionPool {
|
|
|
43
44
|
|
|
44
45
|
// schedule new timer
|
|
45
46
|
this.#startCleanTimer()
|
|
46
|
-
},
|
|
47
|
+
}, functionCleanupIdleTimeInMillis)
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
#cleanupPool() {
|
|
@@ -123,6 +123,13 @@ export default class HandlerRunner {
|
|
|
123
123
|
this.#runner = await this.#loadRunner()
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
try {
|
|
127
|
+
return await this.#runner.run(event, context)
|
|
128
|
+
} catch (err) {
|
|
129
|
+
log.error(
|
|
130
|
+
`Unhandled exception in handler '${this.#funOptions.functionKey}'.`,
|
|
131
|
+
)
|
|
132
|
+
throw err
|
|
133
|
+
}
|
|
127
134
|
}
|
|
128
135
|
}
|
|
@@ -11,8 +11,6 @@ export default class InProcessRunner {
|
|
|
11
11
|
|
|
12
12
|
#env = null
|
|
13
13
|
|
|
14
|
-
#functionKey = null
|
|
15
|
-
|
|
16
14
|
#handler = null
|
|
17
15
|
|
|
18
16
|
#servicePath = null
|
|
@@ -20,11 +18,10 @@ export default class InProcessRunner {
|
|
|
20
18
|
#timeout = null
|
|
21
19
|
|
|
22
20
|
constructor(funOptions, env) {
|
|
23
|
-
const { codeDir,
|
|
21
|
+
const { codeDir, handler, servicePath, timeout } = funOptions
|
|
24
22
|
|
|
25
23
|
this.#codeDir = codeDir
|
|
26
24
|
this.#env = env
|
|
27
|
-
this.#functionKey = functionKey
|
|
28
25
|
this.#handler = handler
|
|
29
26
|
this.#servicePath = servicePath
|
|
30
27
|
this.#timeout = timeout
|
|
@@ -48,7 +45,7 @@ export default class InProcessRunner {
|
|
|
48
45
|
|
|
49
46
|
let callback
|
|
50
47
|
|
|
51
|
-
const
|
|
48
|
+
const callbackWrapper = new Promise((res, rej) => {
|
|
52
49
|
callback = (err, data) => {
|
|
53
50
|
if (err === 'Unauthorized') {
|
|
54
51
|
res('Unauthorized')
|
|
@@ -85,29 +82,17 @@ export default class InProcessRunner {
|
|
|
85
82
|
},
|
|
86
83
|
}
|
|
87
84
|
|
|
88
|
-
let result
|
|
89
|
-
|
|
90
85
|
// execute (run) handler
|
|
91
|
-
try
|
|
92
|
-
|
|
93
|
-
} catch {
|
|
94
|
-
throw new Error(`Uncaught error in '${this.#functionKey}' handler.`)
|
|
95
|
-
}
|
|
86
|
+
// no try/catch so that errors bubble up and are logged with root stack traces
|
|
87
|
+
const result = handler(event, lambdaContext, callback)
|
|
96
88
|
|
|
97
|
-
|
|
98
|
-
// if (result == null || typeof result.then !== 'function') {
|
|
99
|
-
// throw new Error(`Synchronous function execution is not supported.`)
|
|
100
|
-
// }
|
|
101
|
-
|
|
102
|
-
const callbacks = [callbackCalled]
|
|
89
|
+
const responses = [callbackWrapper]
|
|
103
90
|
|
|
104
91
|
// Promise was returned
|
|
105
92
|
if (result != null && typeof result.then === 'function') {
|
|
106
|
-
|
|
93
|
+
responses.push(result)
|
|
107
94
|
}
|
|
108
95
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return callbackResult
|
|
96
|
+
return Promise.race(responses)
|
|
112
97
|
}
|
|
113
98
|
}
|
|
@@ -83,7 +83,7 @@ export default class RubyRunner {
|
|
|
83
83
|
|
|
84
84
|
// console.log(input)
|
|
85
85
|
|
|
86
|
-
const
|
|
86
|
+
const { stderr, stdout } = await execa(
|
|
87
87
|
runtime,
|
|
88
88
|
[
|
|
89
89
|
resolve(__dirname, 'invoke.rb'),
|
|
@@ -97,10 +97,6 @@ export default class RubyRunner {
|
|
|
97
97
|
},
|
|
98
98
|
)
|
|
99
99
|
|
|
100
|
-
const result = await ruby
|
|
101
|
-
|
|
102
|
-
const { stderr, stdout } = result
|
|
103
|
-
|
|
104
100
|
if (stderr) {
|
|
105
101
|
// TODO
|
|
106
102
|
|
package/src/utils/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
export { default as checkDockerDaemon } from './checkDockerDaemon.js'
|
|
2
|
+
export { default as checkGoVersion } from './checkGoVersion.js'
|
|
1
3
|
export { default as createApiKey } from './createApiKey.js'
|
|
2
4
|
export { default as createUniqueId } from './createUniqueId.js'
|
|
3
5
|
export { default as detectExecutable } from './detectExecutable.js'
|
|
4
6
|
export { default as formatToClfTime } from './formatToClfTime.js'
|
|
7
|
+
export { default as generateHapiPath } from './generateHapiPath.js'
|
|
5
8
|
export { default as getHttpApiCorsConfig } from './getHttpApiCorsConfig.js'
|
|
6
9
|
export { default as jsonPath } from './jsonPath.js'
|
|
7
10
|
export { default as lowerCaseKeys } from './lowerCaseKeys.js'
|
|
@@ -10,9 +13,7 @@ export { default as parseMultiValueHeaders } from './parseMultiValueHeaders.js'
|
|
|
10
13
|
export { default as parseMultiValueQueryStringParameters } from './parseMultiValueQueryStringParameters.js'
|
|
11
14
|
export { default as parseQueryStringParameters } from './parseQueryStringParameters.js'
|
|
12
15
|
export { default as splitHandlerPathAndName } from './splitHandlerPathAndName.js'
|
|
13
|
-
|
|
14
|
-
export { default as checkGoVersion } from './checkGoVersion.js'
|
|
15
|
-
export { default as generateHapiPath } from './generateHapiPath.js'
|
|
16
|
+
|
|
16
17
|
// export { default as baseImage } from './baseImage.js'
|
|
17
18
|
|
|
18
19
|
const { isArray } = Array
|