serverless-offline 9.0.0 → 9.1.2

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 CHANGED
@@ -7,8 +7,8 @@
7
7
  <a href="https://www.npmjs.com/package/serverless-offline">
8
8
  <img src="https://img.shields.io/npm/v/serverless-offline.svg?style=flat-square">
9
9
  </a>
10
- <a href="https://github.com/dherault/serverless-offline/actions?query=workflow%3ACI">
11
- <img src="https://img.shields.io/github/workflow/status/dherault/serverless-offline/CI?style=flat-square">
10
+ <a href="https://github.com/dherault/serverless-offline/actions/workflows/integrate.yml">
11
+ <img src="https://img.shields.io/github/workflow/status/dherault/serverless-offline/Integrate">
12
12
  </a>
13
13
  <img src="https://img.shields.io/node/v/serverless-offline.svg?style=flat-square">
14
14
  <a href="https://github.com/serverless/serverless">
@@ -29,9 +29,9 @@
29
29
  This [Serverless](https://github.com/serverless/serverless) plugin emulates [AWS λ](https://aws.amazon.com/lambda) and [API Gateway](https://aws.amazon.com/api-gateway) on your local machine to speed up your development cycles.
30
30
  To do so, it starts an HTTP server that handles the request's lifecycle like APIG does and invokes your handlers.
31
31
 
32
- **Features:**
32
+ **Features**
33
33
 
34
- - [Node.js](https://nodejs.org), [Python](https://www.python.org), [Ruby](https://www.ruby-lang.org) and [Go](https://golang.org) λ runtimes.
34
+ - [Node.js](https://nodejs.org), [Python](https://www.python.org), [Ruby](https://www.ruby-lang.org), [Go](https://golang.org), [Java](https://www.java.com) (incl. [Kotlin](https://kotlinlang.org), [Groovy](https://groovy-lang.org), [Scala](https://www.scala-lang.org)) λ runtimes.
35
35
  - Velocity templates support.
36
36
  - Lazy loading of your handler files.
37
37
  - And more: integrations, authorizers, proxies, timeouts, responseParameters, HTTPS, CORS, etc...
@@ -42,6 +42,7 @@ This plugin is updated by its users, I just do maintenance and ensure that PRs a
42
42
 
43
43
  - [Installation](#installation)
44
44
  - [Usage and command line options](#usage-and-command-line-options)
45
+ - [Run modes](#run-modes)
45
46
  - [Usage with `invoke`](#usage-with-invoke)
46
47
  - [The `process.env.IS_OFFLINE` variable](#the-processenvis_offline-variable)
47
48
  - [Docker and Layers](#docker-and-layers)
@@ -84,7 +85,7 @@ Then inside your project's `serverless.yml` file add following entry to the plug
84
85
 
85
86
  It should look something like this:
86
87
 
87
- ```YAML
88
+ ```yml
88
89
  plugins:
89
90
  - serverless-offline
90
91
  ```
@@ -127,6 +128,7 @@ All CLI options are optional:
127
128
  --ignoreJWTSignature When using HttpApi with a JWT authorizer, don't check the signature of the JWT token. This should only be used for local development.
128
129
  --lambdaPort Lambda http port to listen on. Default: 3002
129
130
  --layersDir The directory layers should be stored in. Default: ${codeDir}/.serverless-offline/layers'
131
+ --localEnvironment Copy local environment variables. Default: false
130
132
  --noAuth Turns off all authorizers
131
133
  --noPrependStageInUrl Don't prepend http routes with the stage.
132
134
  --noStripTrailingSlashInUrl Don't strip trailing slash from http routes.
@@ -145,13 +147,12 @@ All CLI options are optional:
145
147
 
146
148
  Any of the CLI options can be added to your `serverless.yml`. For example:
147
149
 
148
- ```
150
+ ```yml
149
151
  custom:
150
152
  serverless-offline:
151
- httpsProtocol: "dev-certs"
153
+ httpsProtocol: 'dev-certs'
152
154
  httpPort: 4000
153
- stageVariables:
154
- foo: "bar"
155
+ foo: 'bar'
155
156
  ```
156
157
 
157
158
  Options passed on the command line override YAML options.
@@ -163,18 +164,64 @@ By default you can send your requests to `http://localhost:3000/`. Please note t
163
164
  But if you send an `application/x-www-form-urlencoded` or a `multipart/form-data` body with an `application/json` (or no) Content-Type, API Gateway won't parse your data (you'll get the ugly raw as input), whereas the plugin will answer 400 (malformed JSON).
164
165
  Please consider explicitly setting your requests' Content-Type and using separate templates.
165
166
 
167
+ ## Run modes
168
+
169
+ ### node.js
170
+
171
+ Lambda handlers for the `node.js` runtime can run in different execution modes with `serverless-offline` and they have subtle differences with a variety of pros and cons. they are mutually exclusive and it is planned to combine the flags into one single flag in the future.
172
+
173
+ #### worker-threads (default)
174
+
175
+ - handlers run in their own context
176
+ - memory is not being shared between handlers, memory consumption is therefore higher
177
+ - memory is being released when handlers reload or after usage
178
+ - environment (process.env) is not being shared across handlers
179
+ - global state is not being shared across handlers
180
+ - easy debugging
181
+
182
+ #### in-process
183
+
184
+ - handlers run in the same context (instance) as `serverless` and `serverless-offline`
185
+ - memory is being shared across lambda handlers as well as with `serverless` and `serverless-offline`
186
+ - no reloading capabilities as it is [currently] not possible to implement for commonjs handlers (without memory leaks) and for esm handlers
187
+ - environment (process.env) is being shared across handlers as well as with `serverless` and `serverless-offline`
188
+ - global state is being shared across lambda handlers as well as with `serverless` and `serverless-offline`
189
+ - easy debugging
190
+
191
+ #### child-processes
192
+
193
+ - handlers run in a separate node.js instance
194
+ - memory is not being shared between handlers, memory consumption is therefore higher
195
+ - memory is being released when handlers reload or after usage
196
+ - environment (process.env) is not being shared across handlers
197
+ - global state is not being shared across handlers
198
+ - debugging more complicated
199
+
200
+ #### docker
201
+
202
+ - handlers run in a docker container
203
+ - memory is not being shared between handlers, memory consumption is therefore higher
204
+ - memory is being released when handlers reload or after usage
205
+ - environment (process.env) is not being shared across handlers
206
+ - global state is not being shared across handlers
207
+ - debugging more complicated
208
+
209
+ ### Python, Ruby, Go, Java (incl. Kotlin, Groovy, Scala)
210
+
211
+ the Lambda handler process is running in a child process.
212
+
166
213
  ## Usage with `invoke`
167
214
 
168
- To use `Lambda.invoke` you need to set the lambda endpoint to the serverless-offline endpoint:
215
+ To use `Lambda.invoke` you need to set the lambda endpoint to the `serverless-offline` endpoint:
169
216
 
170
217
  ```js
218
+ const { env } = require('node:process')
171
219
  const { Lambda } = require('aws-sdk')
172
220
 
173
221
  const lambda = new Lambda({
174
222
  apiVersion: '2015-03-31',
175
- // endpoint needs to be set only if it deviates from the default, e.g. in a dev environment
176
- // process.env.SOME_VARIABLE could be set in e.g. serverless.yml for provider.environment or function.environment
177
- endpoint: process.env.SOME_VARIABLE
223
+ // endpoint needs to be set only if it deviates from the default
224
+ endpoint: env.IS_OFFLINE
178
225
  ? 'http://localhost:3002'
179
226
  : 'https://lambda.us-east-1.amazonaws.com',
180
227
  })
@@ -183,15 +230,33 @@ const lambda = new Lambda({
183
230
  All your lambdas can then be invoked in a handler using
184
231
 
185
232
  ```js
233
+ const { Buffer } = require('node:buffer')
234
+ const { Lambda } = require('aws-sdk')
235
+
236
+ const { stringify } = JSON
237
+
238
+ const lambda = new Lambda({
239
+ apiVersion: '2015-03-31',
240
+ endpoint: 'http://localhost:3002',
241
+ })
242
+
186
243
  exports.handler = async function () {
244
+ const clientContextData = stringify({ foo: 'foo' })
245
+
187
246
  const params = {
247
+ ClientContext: Buffer.from(clientContextData).toString('base64'),
188
248
  // FunctionName is composed of: service name - stage - function name, e.g.
189
249
  FunctionName: 'myServiceName-dev-invokedHandler',
190
250
  InvocationType: 'RequestResponse',
191
- Payload: JSON.stringify({ data: 'foo' }),
251
+ Payload: stringify({ data: 'foo' }),
192
252
  }
193
253
 
194
254
  const response = await lambda.invoke(params).promise()
255
+
256
+ return {
257
+ body: stringify(response),
258
+ statusCode: 200,
259
+ }
195
260
  }
196
261
  ```
197
262
 
@@ -243,7 +308,7 @@ to calling it via `aws-sdk`.
243
308
 
244
309
  ## The `process.env.IS_OFFLINE` variable
245
310
 
246
- Will be `"true"` in your handlers and throughout the plugin.
311
+ Will be `"true"` in your handlers when using `serverless-offline`.
247
312
 
248
313
  ## Docker and Layers
249
314
 
@@ -266,14 +331,14 @@ If you're using least-privilege principals for your AWS roles, this policy shoul
266
331
 
267
332
  ```json
268
333
  {
269
- "Version": "2012-10-17",
270
334
  "Statement": [
271
335
  {
272
- "Effect": "Allow",
273
336
  "Action": "lambda:GetLayerVersion",
337
+ "Effect": "Allow",
274
338
  "Resource": "arn:aws:lambda:*:*:layer:*:*"
275
339
  }
276
- ]
340
+ ],
341
+ "Version": "2012-10-17"
277
342
  }
278
343
  ```
279
344
 
@@ -325,11 +390,11 @@ Only [custom authorizers](https://aws.amazon.com/blogs/compute/introducing-custo
325
390
 
326
391
  The Custom authorizer is passed an `event` object as below:
327
392
 
328
- ```javascript
393
+ ```js
329
394
  {
330
- "type": "TOKEN",
331
395
  "authorizationToken": "<Incoming bearer token>",
332
- "methodArn": "arn:aws:execute-api:<Region id>:<Account id>:<API id>/<Stage>/<Method>/<Resource path>"
396
+ "methodArn": "arn:aws:execute-api:<Region id>:<Account id>:<API id>/<Stage>/<Method>/<Resource path>",
397
+ "type": "TOKEN"
333
398
  }
334
399
  ```
335
400
 
@@ -337,11 +402,11 @@ The `methodArn` does not include the Account id or API id.
337
402
 
338
403
  The plugin only supports retrieving Tokens from headers. You can configure the header as below:
339
404
 
340
- ```javascript
405
+ ```js
341
406
  "authorizer": {
342
- "type": "TOKEN",
407
+ "authorizerResultTtlInSeconds": "0",
343
408
  "identitySource": "method.request.header.Authorization", // or method.request.header.SomeOtherHeader
344
- "authorizerResultTtlInSeconds": "0"
409
+ "type": "TOKEN"
345
410
  }
346
411
  ```
347
412
 
@@ -369,14 +434,14 @@ If your authentication needs are custom and not satisfied by the existing capabi
369
434
  ```js
370
435
  module.exports = function (endpoint, functionKey, method, path) {
371
436
  return {
372
- name: 'your strategy name',
373
- scheme: 'your scheme name',
374
-
375
437
  getAuthenticateFunction: () => ({
376
438
  async authenticate(request, h) {
377
439
  // your implementation
378
440
  },
379
441
  }),
442
+
443
+ name: 'your strategy name',
444
+ scheme: 'your scheme name',
380
445
  }
381
446
  }
382
447
  ```
@@ -440,7 +505,7 @@ Now let's make a request with this body: `{ "id": 1 }`
440
505
 
441
506
  AWS parses the event as such:
442
507
 
443
- ```javascript
508
+ ```js
444
509
  {
445
510
  "payload": {
446
511
  "id": 1
@@ -452,7 +517,7 @@ AWS parses the event as such:
452
517
 
453
518
  Whereas Offline parses:
454
519
 
455
- ```javascript
520
+ ```js
456
521
  {
457
522
  "payload": {
458
523
  "id": 1
@@ -504,7 +569,7 @@ Works out of the box. See examples in the manual_test directory.
504
569
 
505
570
  Example of enabling proxy:
506
571
 
507
- ```
572
+ ```yml
508
573
  custom:
509
574
  serverless-offline:
510
575
  resourceRoutes: true
@@ -512,18 +577,18 @@ custom:
512
577
 
513
578
  or
514
579
 
515
- ```
580
+ ```yml
516
581
  YourCloudFormationMethodId:
517
- Type: AWS::ApiGateway::Method
518
582
  Properties:
519
583
  ......
520
584
  Integration:
521
585
  Type: HTTP_PROXY
522
586
  Uri: 'https://s3-${self:custom.region}.amazonaws.com/${self:custom.yourBucketName}/{proxy}'
523
587
  ......
588
+ Type: AWS::ApiGateway::Method
524
589
  ```
525
590
 
526
- ```
591
+ ```yml
527
592
  custom:
528
593
  serverless-offline:
529
594
  resourceRoutes:
@@ -541,7 +606,7 @@ May not work properly. Please PR. (Difficulty: hard?)
541
606
 
542
607
  Example response velocity template:
543
608
 
544
- ```javascript
609
+ ```js
545
610
  "responseParameters": {
546
611
  "method.response.header.X-Powered-By": "Serverless", // a string
547
612
  "method.response.header.Warning": "integration.response.body", // the whole response
@@ -558,7 +623,9 @@ Usage in order to send messages back to clients:
558
623
  Or,
559
624
 
560
625
  ```js
561
- const apiGatewayManagementApi = new AWS.ApiGatewayManagementApi({
626
+ const { ApiGatewayManagementApi } = require('aws-sdk')
627
+
628
+ const apiGatewayManagementApi = new ApiGatewayManagementApi({
562
629
  apiVersion: '2018-11-29',
563
630
  endpoint: 'http://localhost:3001',
564
631
  });
@@ -603,13 +670,13 @@ Add a new [launch configuration](https://code.visualstudio.com/docs/editor/debug
603
670
 
604
671
  ```json
605
672
  {
606
- "type": "node",
607
- "request": "launch",
608
- "name": "Debug Serverless Offline",
609
673
  "cwd": "${workspaceFolder}",
610
- "runtimeExecutable": "npm",
674
+ "name": "Debug Serverless Offline",
675
+ "request": "launch",
611
676
  "runtimeArgs": ["run", "debug"],
612
- "sourceMaps": true
677
+ "runtimeExecutable": "npm",
678
+ "sourceMaps": true,
679
+ "type": "node"
613
680
  }
614
681
  ```
615
682
 
@@ -647,7 +714,7 @@ You can change this profile directly in the code or by setting proper environmen
647
714
  ## Simulation quality
648
715
 
649
716
  This plugin simulates API Gateway for many practical purposes, good enough for development - but is not a perfect simulator.
650
- Specifically, Lambda currently runs on Node.js v10.x, v12.x and v14.x ([AWS Docs](https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html)), whereas _Offline_ runs on your own runtime where no memory limits are enforced.
717
+ Specifically, Lambda currently runs on Node.js v12.x, v14.x and v16.x ([AWS Docs](https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html)), whereas _Offline_ runs on your own runtime where no memory limits are enforced.
651
718
 
652
719
  ## Usage with other plugins
653
720
 
@@ -660,7 +727,7 @@ Plugins are executed in order, so plugins that process your code or add resource
660
727
 
661
728
  For example:
662
729
 
663
- ```yaml
730
+ ```yml
664
731
  plugins:
665
732
  - serverless-middleware # modifies some of your handler based on configuration
666
733
  - serverless-webpack # package your javascript handlers using webpack
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "dedicatedTo": "Blue, a great migrating bird.",
3
3
  "name": "serverless-offline",
4
- "version": "9.0.0",
4
+ "version": "9.1.2",
5
5
  "description": "Emulate AWS λ and API Gateway locally when developing your Serverless project",
6
6
  "license": "MIT",
7
7
  "main": "./src/index.js",
@@ -194,7 +194,7 @@
194
194
  "@hapi/boom": "^10.0.0",
195
195
  "@hapi/h2o2": "^9.1.0",
196
196
  "@hapi/hapi": "^20.2.2",
197
- "aws-sdk": "^2.1176.0",
197
+ "aws-sdk": "^2.1185.0",
198
198
  "boxen": "^7.0.0",
199
199
  "chalk": "^5.0.1",
200
200
  "execa": "^6.1.0",
@@ -206,7 +206,7 @@
206
206
  "jsonwebtoken": "^8.5.1",
207
207
  "jszip": "^3.10.0",
208
208
  "luxon": "^3.0.1",
209
- "node-fetch": "^3.2.8",
209
+ "node-fetch": "^3.2.10",
210
210
  "node-schedule": "^2.1.0",
211
211
  "object.hasown": "^1.1.1",
212
212
  "p-memoize": "^7.1.0",
@@ -1,4 +1,4 @@
1
- import process, { env, exit } from 'node:process'
1
+ import process, { exit } from 'node:process'
2
2
  import { log } from '@serverless/utils/log.js'
3
3
  import chalk from 'chalk'
4
4
  import {
@@ -59,9 +59,6 @@ export default class ServerlessOffline {
59
59
 
60
60
  // Entry point for the plugin (sls offline) when running 'sls offline start'
61
61
  async start() {
62
- // Put here so available everywhere, not just in handlers
63
- env.IS_OFFLINE = true
64
-
65
62
  this.#mergeOptions()
66
63
 
67
64
  const { httpEvents, lambdas, scheduleEvents, webSocketEvents } =
@@ -91,6 +91,10 @@ export default {
91
91
  usage:
92
92
  'The directory layers should be stored in. Default: {codeDir}/.serverless-offline/layers',
93
93
  },
94
+ localEnvironment: {
95
+ type: 'boolean',
96
+ usage: 'Copy local environment variables. Default: false',
97
+ },
94
98
  noAuth: {
95
99
  type: 'boolean',
96
100
  usage: 'Turns off all authorizers',
@@ -4,12 +4,12 @@ export const BASE_URL_PLACEHOLDER = 'http://example'
4
4
 
5
5
  export const CUSTOM_OPTION = 'serverless-offline'
6
6
 
7
- export const DEFAULT_LAMBDA_RUNTIME = 'nodejs12.x'
7
+ export const DEFAULT_LAMBDA_RUNTIME = 'nodejs14.x'
8
8
 
9
9
  // https://docs.aws.amazon.com/lambda/latest/dg/limits.html
10
10
  export const DEFAULT_LAMBDA_MEMORY_SIZE = 1024
11
11
  // default function timeout in seconds
12
- export const DEFAULT_LAMBDA_TIMEOUT = 900 // 15 min
12
+ export const DEFAULT_LAMBDA_TIMEOUT = 6 // 6 seconds
13
13
 
14
14
  // timeout for all connections to be closed
15
15
  export const SERVER_SHUTDOWN_TIMEOUT = 5000
@@ -20,6 +20,7 @@ export default {
20
20
  httpsProtocol: '',
21
21
  lambdaPort: 3002,
22
22
  layersDir: null,
23
+ localEnvironment: false,
23
24
  noAuth: false,
24
25
  noPrependStageInUrl: false,
25
26
  noStripTrailingSlashInUrl: false,
@@ -2,7 +2,7 @@ import { Buffer } from 'node:buffer'
2
2
  import { readFileSync } from 'node:fs'
3
3
  import { createRequire } from 'node:module'
4
4
  import { join, resolve } from 'node:path'
5
- import process, { exit } from 'node:process'
5
+ import { exit } from 'node:process'
6
6
  import h2o2 from '@hapi/h2o2'
7
7
  import { Server } from '@hapi/hapi'
8
8
  import { log } from '@serverless/utils/log.js'
@@ -35,8 +35,6 @@ const { assign, entries, keys } = Object
35
35
  export default class HttpServer {
36
36
  #lambda = null
37
37
 
38
- #lastRequestOptions = null
39
-
40
38
  #options = null
41
39
 
42
40
  #serverless = null
@@ -198,17 +196,6 @@ export default class HttpServer {
198
196
  const server = `${httpsProtocol ? 'https' : 'http'}://${host}:${httpPort}`
199
197
 
200
198
  log.notice(`Server ready: ${server} 🚀`)
201
- log.notice()
202
- log.notice('Enter "rp" to replay the last request')
203
-
204
- process.openStdin().addListener('data', (data) => {
205
- // note: data is an object, and when converted to a string it will
206
- // end with a linefeed. so we (rather crudely) account for that
207
- // with toString() and then trim()
208
- if (data.toString().trim() === 'rp') {
209
- this.#injectLastRequest()
210
- }
211
- })
212
199
  }
213
200
 
214
201
  // stops the server
@@ -513,24 +500,11 @@ export default class HttpServer {
513
500
  hapiOptions.tags = ['api']
514
501
 
515
502
  const hapiHandler = async (request, h) => {
516
- // Here we go
517
- // Store current request as the last one
518
- this.#lastRequestOptions = {
519
- headers: request.headers,
520
- method: request.method,
521
- payload: request.payload,
522
- url: request.url.href,
523
- }
524
-
525
503
  const requestPath =
526
504
  endpoint.isHttpApi || this.#options.noPrependStageInUrl
527
505
  ? request.path
528
506
  : request.path.substr(`/${stage}`.length)
529
507
 
530
- if (request.auth.credentials && request.auth.strategy) {
531
- this.#lastRequestOptions.auth = request.auth
532
- }
533
-
534
508
  // Payload processing
535
509
  const encoding = detectEncoding(request)
536
510
 
@@ -667,24 +641,18 @@ export default class HttpServer {
667
641
  event = request.payload || {}
668
642
  }
669
643
  } else if (integration === 'AWS_PROXY') {
670
- const stageVariables = this.#serverless.service.custom
671
- ? this.#serverless.service.custom.stageVariables
672
- : null
673
-
674
644
  const lambdaProxyIntegrationEvent =
675
645
  endpoint.isHttpApi && endpoint.payload === '2.0'
676
646
  ? new LambdaProxyIntegrationEventV2(
677
647
  request,
678
648
  stage,
679
649
  endpoint.routeKey,
680
- stageVariables,
681
650
  additionalRequestContext,
682
651
  )
683
652
  : new LambdaProxyIntegrationEvent(
684
653
  request,
685
654
  stage,
686
655
  requestPath,
687
- stageVariables,
688
656
  endpoint.isHttpApi ? endpoint.routeKey : null,
689
657
  additionalRequestContext,
690
658
  )
@@ -1257,15 +1225,6 @@ export default class HttpServer {
1257
1225
  .map((line) => line.trim())
1258
1226
  }
1259
1227
 
1260
- #injectLastRequest() {
1261
- if (this.#lastRequestOptions) {
1262
- log.notice('Replaying HTTP last request')
1263
- this.#server.inject(this.#lastRequestOptions)
1264
- } else {
1265
- log.notice('No last HTTP request to replay!')
1266
- }
1267
- }
1268
-
1269
1228
  writeRoutesTerminal() {
1270
1229
  logRoutes(this.#terminalInfo)
1271
1230
  }
@@ -30,22 +30,12 @@ export default class LambdaProxyIntegrationEvent {
30
30
 
31
31
  #stage = null
32
32
 
33
- #stageVariables = null
34
-
35
- constructor(
36
- request,
37
- stage,
38
- path,
39
- stageVariables,
40
- routeKey,
41
- additionalRequestContext,
42
- ) {
33
+ constructor(request, stage, path, routeKey, additionalRequestContext) {
43
34
  this.#additionalRequestContext = additionalRequestContext || {}
44
35
  this.#path = path
45
36
  this.#routeKey = routeKey
46
37
  this.#request = request
47
38
  this.#stage = stage
48
- this.#stageVariables = stageVariables
49
39
  }
50
40
 
51
41
  create() {
@@ -227,7 +217,7 @@ export default class LambdaProxyIntegrationEvent {
227
217
  stage: this.#stage,
228
218
  },
229
219
  resource,
230
- stageVariables: this.#stageVariables,
220
+ stageVariables: null,
231
221
  }
232
222
  }
233
223
  }
@@ -24,20 +24,11 @@ export default class LambdaProxyIntegrationEventV2 {
24
24
 
25
25
  #stage = null
26
26
 
27
- #stageVariables = null
28
-
29
- constructor(
30
- request,
31
- stage,
32
- routeKey,
33
- stageVariables,
34
- additionalRequestContext,
35
- ) {
27
+ constructor(request, stage, routeKey, additionalRequestContext) {
36
28
  this.#additionalRequestContext = additionalRequestContext || {}
37
29
  this.#routeKey = routeKey
38
30
  this.#request = request
39
31
  this.#stage = stage
40
- this.#stageVariables = stageVariables
41
32
  }
42
33
 
43
34
  create() {
@@ -183,7 +174,7 @@ export default class LambdaProxyIntegrationEventV2 {
183
174
  timeEpoch: requestTimeEpoch,
184
175
  },
185
176
  routeKey: this.#routeKey,
186
- stageVariables: this.#stageVariables,
177
+ stageVariables: null,
187
178
  version: '2.0',
188
179
  }
189
180
  }
@@ -39,9 +39,9 @@ export default class Schedule {
39
39
  const cron = this.#convertExpressionToCron(entry)
40
40
 
41
41
  log.notice(
42
- `Scheduling [${functionKey}] cron: [${cron}] input: ${stringify(
43
- input,
44
- )}`,
42
+ `Scheduling [${functionKey}] cron: [${cron}]${
43
+ input ? ` input: ${stringify(input)}` : ''
44
+ }`,
45
45
  )
46
46
 
47
47
  nodeSchedule.scheduleJob(cron, async () => {
@@ -1,12 +1,12 @@
1
1
  import { readFile, writeFile } from 'node:fs/promises'
2
2
  import { dirname, join, resolve } from 'node:path'
3
+ import process from 'node:process'
3
4
  import { performance } from 'node:perf_hooks'
4
5
  import { log } from '@serverless/utils/log.js'
5
6
  import { emptyDir, ensureDir, remove } from 'fs-extra'
6
7
  import jszip from 'jszip'
7
8
  import HandlerRunner from './handler-runner/index.js'
8
9
  import LambdaContext from './LambdaContext.js'
9
- import resolveJoins from '../utils/resolveJoins.js'
10
10
  import {
11
11
  DEFAULT_LAMBDA_MEMORY_SIZE,
12
12
  DEFAULT_LAMBDA_RUNTIME,
@@ -15,8 +15,8 @@ import {
15
15
  } from '../config/index.js'
16
16
  import { createUniqueId, splitHandlerPathAndName } from '../utils/index.js'
17
17
 
18
- const { entries } = Object
19
18
  const { ceil } = Math
19
+ const { entries, fromEntries } = Object
20
20
 
21
21
  export default class LambdaFunction {
22
22
  #artifact = null
@@ -35,6 +35,8 @@ export default class LambdaFunction {
35
35
 
36
36
  #functionName = null
37
37
 
38
+ #handler = null
39
+
38
40
  #handlerRunner = null
39
41
 
40
42
  #idleTimeStarted = null
@@ -86,6 +88,7 @@ export default class LambdaFunction {
86
88
  // this._executionTimeout = null
87
89
  this.#functionKey = functionKey
88
90
  this.#functionName = name
91
+ this.#handler = handler
89
92
  this.#memorySize = memorySize
90
93
  this.#region = provider.region
91
94
  this.#runtime = runtime
@@ -93,11 +96,18 @@ export default class LambdaFunction {
93
96
 
94
97
  this.#verifySupportedRuntime()
95
98
 
96
- const env = this.#getEnv(
97
- resolveJoins(provider.environment),
98
- functionDefinition.environment,
99
- handler,
100
- )
99
+ const env = {
100
+ ...(options.localEnvironment
101
+ ? process.env
102
+ : // we always copy all AWS_xxxx environment variables over from local env
103
+ fromEntries(
104
+ entries(process.env).filter(([key]) => key.startsWith('AWS_')),
105
+ )),
106
+ ...this.#getAwsEnvVars(),
107
+ ...provider.environment,
108
+ ...functionDefinition.environment,
109
+ IS_OFFLINE: 'true',
110
+ }
101
111
 
102
112
  this.#artifact = functionDefinition.package?.artifact
103
113
 
@@ -176,6 +186,7 @@ export default class LambdaFunction {
176
186
  // https://github.com/serverless/serverless/blob/v1.50.0/lib/plugins/aws/invokeLocal/index.js#L108
177
187
  #getAwsEnvVars() {
178
188
  return {
189
+ _HANDLER: this.#handler,
179
190
  AWS_DEFAULT_REGION: this.#region,
180
191
  AWS_LAMBDA_FUNCTION_MEMORY_SIZE: this.#memorySize,
181
192
  AWS_LAMBDA_FUNCTION_NAME: this.#functionName,
@@ -194,16 +205,6 @@ export default class LambdaFunction {
194
205
  }
195
206
  }
196
207
 
197
- #getEnv(providerEnv, functionDefinitionEnv, handler) {
198
- return {
199
- ...this.#getAwsEnvVars(),
200
- ...providerEnv,
201
- ...functionDefinitionEnv,
202
- _HANDLER: handler, // TODO is this available in AWS?
203
- IS_OFFLINE: true,
204
- }
205
- }
206
-
207
208
  setClientContext(clientContext) {
208
209
  this.#clientContext = clientContext
209
210
  }
@@ -41,14 +41,18 @@ export default class ChildProcessRunner {
41
41
  },
42
42
  )
43
43
 
44
- const message = new Promise((res, rej) => {
45
- childProcess.on('message', (data) => {
46
- if (data.error) rej(data.error)
47
- else res(data)
44
+ let message
45
+
46
+ try {
47
+ message = new Promise((res, rej) => {
48
+ childProcess.on('message', (data) => {
49
+ if (data.error) rej(data.error)
50
+ else res(data)
51
+ })
48
52
  })
49
- }).finally(() => {
53
+ } finally {
50
54
  childProcess.kill()
51
- })
55
+ }
52
56
 
53
57
  childProcess.send({
54
58
  context,
@@ -12,6 +12,8 @@ import pRetry from 'p-retry'
12
12
  import DockerImage from './DockerImage.js'
13
13
 
14
14
  const { stringify } = JSON
15
+ const { floor, log: mathLog } = Math
16
+ const { parseFloat } = Number
15
17
  const { entries, hasOwn } = Object
16
18
 
17
19
  export default class DockerContainer {
@@ -402,7 +404,7 @@ export default class DockerContainer {
402
404
  const dm = decimals < 0 ? 0 : decimals
403
405
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
404
406
 
405
- const i = Math.floor(Math.log(bytes) / Math.log(k))
407
+ const i = floor(mathLog(bytes) / mathLog(k))
406
408
 
407
409
  return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
408
410
  }
@@ -1,15 +1,15 @@
1
- import { mkdir, readFile, rmdir, writeFile } from 'node:fs/promises'
1
+ import { mkdir, readFile, rm, rmdir, writeFile } from 'node:fs/promises'
2
2
  import { EOL } from 'node:os'
3
- import { sep, resolve, parse as pathParse } from 'node:path'
4
3
  import process, { chdir, cwd } from 'node:process'
4
+ import { parse as pathParse, resolve, sep } from 'node:path'
5
5
  import { log } from '@serverless/utils/log.js'
6
- import { execa, execaSync } from 'execa'
6
+ import { execa } from 'execa'
7
7
 
8
8
  const { parse, stringify } = JSON
9
9
 
10
- const PAYLOAD_IDENTIFIER = 'offline_payload'
11
-
12
10
  export default class GoRunner {
11
+ static #payloadIdentifier = 'offline_payload'
12
+
13
13
  #codeDir = null
14
14
 
15
15
  #env = null
@@ -18,10 +18,10 @@ export default class GoRunner {
18
18
 
19
19
  #handlerPath = null
20
20
 
21
- #tmpPath = null
22
-
23
21
  #tmpFile = null
24
22
 
23
+ #tmpPath = null
24
+
25
25
  constructor(funOptions, env) {
26
26
  const { handlerPath, codeDir } = funOptions
27
27
 
@@ -32,7 +32,12 @@ export default class GoRunner {
32
32
 
33
33
  async cleanup() {
34
34
  try {
35
- await rmdir(this.#tmpPath, { recursive: true })
35
+ // refresh go.mod
36
+ await rm(this.#tmpFile)
37
+ await execa('go', ['mod', 'tidy'])
38
+ await rmdir(this.#tmpPath, {
39
+ recursive: true,
40
+ })
36
41
  } catch {
37
42
  // @ignore
38
43
  }
@@ -46,12 +51,10 @@ export default class GoRunner {
46
51
  let payload
47
52
 
48
53
  for (const item of value.split(EOL)) {
49
- if (item.indexOf(PAYLOAD_IDENTIFIER) === -1) {
50
- logs.push(item)
51
- } else if (item.indexOf(PAYLOAD_IDENTIFIER) !== -1) {
54
+ if (item.includes(GoRunner.#payloadIdentifier)) {
52
55
  try {
53
56
  const {
54
- offline_payload: { success, error },
57
+ [GoRunner.#payloadIdentifier]: { error, success },
55
58
  } = parse(item)
56
59
 
57
60
  if (success) {
@@ -62,6 +65,8 @@ export default class GoRunner {
62
65
  } catch {
63
66
  // @ignore
64
67
  }
68
+ } else {
69
+ logs.push(item)
65
70
  }
66
71
  }
67
72
 
@@ -119,8 +124,11 @@ export default class GoRunner {
119
124
  chdir(cwdPath.substring(0, cwdPath.indexOf('main.go')))
120
125
 
121
126
  // Make sure we have the mock-lambda runner
122
- execaSync('go', ['get', 'github.com/icarus-sullivan/mock-lambda@e065469'])
123
- execaSync('go', ['build'])
127
+ await execa('go', [
128
+ 'get',
129
+ 'github.com/icarus-sullivan/mock-lambda@e065469',
130
+ ])
131
+ await execa('go', ['build'])
124
132
  } catch {
125
133
  // @ignore
126
134
  }
@@ -154,8 +162,6 @@ export default class GoRunner {
154
162
  }
155
163
 
156
164
  try {
157
- // refresh go.mod
158
- execaSync('go', ['mod', 'tidy'])
159
165
  chdir(this.#codeDir)
160
166
  } catch {
161
167
  // @ignore
@@ -3,6 +3,7 @@ import { performance } from 'node:perf_hooks'
3
3
  import process from 'node:process'
4
4
  import { log } from '@serverless/utils/log.js'
5
5
 
6
+ const { floor } = Math
6
7
  const { assign } = Object
7
8
 
8
9
  const require = createRequire(import.meta.url)
@@ -88,11 +89,11 @@ export default class InProcessRunner {
88
89
  ...context,
89
90
  done: (err, data) => callback(err, data),
90
91
  fail: (err) => callback(err),
91
- getRemainingTimeInMillis: () => {
92
+ getRemainingTimeInMillis() {
92
93
  const timeLeft = executionTimeout - performance.now()
93
94
 
94
95
  // just return 0 for now if we are beyond alotted time (timeout)
95
- return timeLeft > 0 ? timeLeft : 0
96
+ return timeLeft > 0 ? floor(timeLeft) : 0
96
97
  },
97
98
  succeed: (res) => callback(null, res),
98
99
  }
@@ -4,9 +4,11 @@ import { log } from '@serverless/utils/log.js'
4
4
  import { invokeJavaLocal } from 'java-invoke-local'
5
5
 
6
6
  const { parse, stringify } = JSON
7
- const { has } = Reflect
7
+ const { hasOwn } = Object
8
8
 
9
9
  export default class JavaRunner {
10
+ static #payloadIdentifier = '__offline_payload__'
11
+
10
12
  #deployPackage = null
11
13
 
12
14
  #env = null
@@ -45,9 +47,9 @@ export default class JavaRunner {
45
47
  if (
46
48
  json &&
47
49
  typeof json === 'object' &&
48
- has(json, '__offline_payload__')
50
+ hasOwn(json, JavaRunner.#payloadIdentifier)
49
51
  ) {
50
- return json.__offline_payload__
52
+ return json[JavaRunner.#payloadIdentifier]
51
53
  }
52
54
  }
53
55
 
@@ -7,12 +7,13 @@ import { fileURLToPath } from 'node:url'
7
7
  import { log } from '@serverless/utils/log.js'
8
8
 
9
9
  const { parse, stringify } = JSON
10
- const { assign } = Object
11
- const { has } = Reflect
10
+ const { assign, hasOwn } = Object
12
11
 
13
12
  const __dirname = dirname(fileURLToPath(import.meta.url))
14
13
 
15
14
  export default class PythonRunner {
15
+ static #payloadIdentifier = '__offline_payload__'
16
+
16
17
  #env = null
17
18
 
18
19
  #handlerName = null
@@ -83,9 +84,9 @@ export default class PythonRunner {
83
84
  if (
84
85
  json &&
85
86
  typeof json === 'object' &&
86
- has(json, '__offline_payload__')
87
+ hasOwn(json, PythonRunner.#payloadIdentifier)
87
88
  ) {
88
- payload = json.__offline_payload__
89
+ payload = json[PythonRunner.#payloadIdentifier]
89
90
  // everything else is print(), logging, ...
90
91
  } else {
91
92
  log.notice(item)
@@ -6,11 +6,13 @@ import { log } from '@serverless/utils/log.js'
6
6
  import { execa } from 'execa'
7
7
 
8
8
  const { parse, stringify } = JSON
9
- const { has } = Reflect
9
+ const { hasOwn } = Object
10
10
 
11
11
  const __dirname = dirname(fileURLToPath(import.meta.url))
12
12
 
13
13
  export default class RubyRunner {
14
+ static #payloadIdentifier = '__offline_payload__'
15
+
14
16
  #env = null
15
17
 
16
18
  #handlerName = null
@@ -47,9 +49,9 @@ export default class RubyRunner {
47
49
  if (
48
50
  json &&
49
51
  typeof json === 'object' &&
50
- has(json, '__offline_payload__')
52
+ hasOwn(json, RubyRunner.#payloadIdentifier)
51
53
  ) {
52
- payload = json.__offline_payload__
54
+ payload = json[RubyRunner.#payloadIdentifier]
53
55
  } else {
54
56
  log.notice(item)
55
57
  }
@@ -2,10 +2,10 @@ import { env } from 'node:process'
2
2
  import { parentPort, workerData } from 'node:worker_threads'
3
3
  import InProcessRunner from '../in-process-runner/index.js'
4
4
 
5
- const { functionKey, handlerName, handlerPath } = workerData
5
+ const { functionKey, handlerName, handlerPath, timeout } = workerData
6
6
 
7
7
  parentPort.on('message', async (messageData) => {
8
- const { context, event, port, timeout } = messageData
8
+ const { context, event, port } = messageData
9
9
 
10
10
  // TODO we could probably cache this in the module scope?
11
11
  const inProcessRunner = new InProcessRunner(
@@ -1,29 +0,0 @@
1
- const { entries } = Object
2
-
3
- // Used to resolve Fn::Join in environment variables
4
- export default function resolveJoins(environment) {
5
- if (!environment) {
6
- return undefined
7
- }
8
-
9
- const newEnv = {}
10
-
11
- entries(environment).forEach(([key, value]) => {
12
- if (!value) {
13
- return
14
- }
15
-
16
- const joinArray = value['Fn::Join']
17
- const isJoin = Boolean(joinArray)
18
-
19
- if (isJoin) {
20
- const separator = joinArray[0]
21
- const joined = joinArray[1].join(separator)
22
- newEnv[key] = joined
23
- } else {
24
- newEnv[key] = value
25
- }
26
- })
27
-
28
- return newEnv
29
- }