serverless-offline 8.7.0 → 9.0.0
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 +91 -95
- package/package.json +41 -69
- package/src/ServerlessOffline.js +412 -0
- package/src/config/commandOptions.js +155 -0
- package/src/config/constants.js +22 -0
- package/{dist → src}/config/defaultOptions.js +8 -17
- package/src/config/index.js +4 -0
- package/src/config/supportedRuntimes.js +47 -0
- package/src/events/authCanExecuteResource.js +35 -0
- package/src/events/authFunctionNameExtractor.js +75 -0
- package/src/events/authMatchPolicyResource.js +71 -0
- package/src/events/authValidateContext.js +51 -0
- package/src/events/http/Endpoint.js +135 -0
- package/src/events/http/Http.js +50 -0
- package/src/events/http/HttpEventDefinition.js +20 -0
- package/src/events/http/HttpServer.js +1277 -0
- package/src/events/http/OfflineEndpoint.js +33 -0
- package/src/events/http/authJWTSettingsExtractor.js +70 -0
- package/src/events/http/createAuthScheme.js +176 -0
- package/src/events/http/createJWTAuthScheme.js +106 -0
- package/src/events/http/index.js +1 -0
- package/src/events/http/javaHelpers.js +102 -0
- package/src/events/http/lambda-events/LambdaIntegrationEvent.js +57 -0
- package/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js +233 -0
- package/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js +190 -0
- package/src/events/http/lambda-events/VelocityContext.js +147 -0
- package/src/events/http/lambda-events/index.js +4 -0
- package/src/events/http/lambda-events/renderVelocityTemplateObject.js +93 -0
- package/{dist → src}/events/http/parseResources.js +73 -78
- package/src/events/http/payloadSchemaValidator.js +13 -0
- package/{dist → src}/events/http/templates/offline-default.req.vm +0 -0
- package/{dist → src}/events/http/templates/offline-default.res.vm +0 -0
- package/src/events/schedule/Schedule.js +131 -0
- package/src/events/schedule/ScheduleEvent.js +18 -0
- package/src/events/schedule/ScheduleEventDefinition.js +21 -0
- package/src/events/schedule/index.js +1 -0
- package/src/events/websocket/HttpServer.js +69 -0
- package/src/events/websocket/WebSocket.js +52 -0
- package/src/events/websocket/WebSocketClients.js +462 -0
- package/src/events/websocket/WebSocketEventDefinition.js +18 -0
- package/src/events/websocket/WebSocketServer.js +73 -0
- package/src/events/websocket/http-routes/_catchAll/catchAllRoute.js +16 -0
- package/src/events/websocket/http-routes/_catchAll/index.js +1 -0
- package/src/events/websocket/http-routes/connections/ConnectionsController.js +28 -0
- package/src/events/websocket/http-routes/connections/connectionsRoutes.js +70 -0
- package/src/events/websocket/http-routes/connections/index.js +1 -0
- package/src/events/websocket/http-routes/index.js +2 -0
- package/src/events/websocket/index.js +1 -0
- package/src/events/websocket/lambda-events/WebSocketAuthorizerEvent.js +65 -0
- package/src/events/websocket/lambda-events/WebSocketConnectEvent.js +68 -0
- package/src/events/websocket/lambda-events/WebSocketDisconnectEvent.js +31 -0
- package/src/events/websocket/lambda-events/WebSocketEvent.js +29 -0
- package/src/events/websocket/lambda-events/WebSocketRequestContext.js +67 -0
- package/src/events/websocket/lambda-events/index.js +4 -0
- package/src/index.js +12 -0
- package/src/lambda/HttpServer.js +108 -0
- package/src/lambda/Lambda.js +68 -0
- package/src/lambda/LambdaContext.js +33 -0
- package/src/lambda/LambdaFunction.js +308 -0
- package/src/lambda/LambdaFunctionPool.js +109 -0
- package/src/lambda/__tests__/LambdaContext.test.js +30 -0
- package/src/lambda/__tests__/LambdaFunction.test.js +196 -0
- package/src/lambda/__tests__/fixtures/Lambda/LambdaFunctionThatReturnsJSONObject.fixture.js +47 -0
- package/src/lambda/__tests__/fixtures/Lambda/LambdaFunctionThatReturnsNativeString.fixture.js +46 -0
- package/src/lambda/__tests__/fixtures/Lambda/package.json +3 -0
- package/src/lambda/__tests__/fixtures/lambdaFunction.fixture.js +145 -0
- package/src/lambda/__tests__/fixtures/package.json +3 -0
- package/src/lambda/__tests__/routes/invocations/InvocationsController.test.js +42 -0
- package/src/lambda/handler-runner/HandlerRunner.js +136 -0
- package/src/lambda/handler-runner/child-process-runner/ChildProcessRunner.js +72 -0
- package/src/lambda/handler-runner/child-process-runner/childProcessHelper.js +42 -0
- package/src/lambda/handler-runner/child-process-runner/index.js +1 -0
- package/src/lambda/handler-runner/docker-runner/DockerContainer.js +417 -0
- package/src/lambda/handler-runner/docker-runner/DockerImage.js +35 -0
- package/src/lambda/handler-runner/docker-runner/DockerRunner.js +63 -0
- package/src/lambda/handler-runner/docker-runner/index.js +1 -0
- package/src/lambda/handler-runner/go-runner/GoRunner.js +166 -0
- package/src/lambda/handler-runner/go-runner/index.js +1 -0
- package/src/lambda/handler-runner/in-process-runner/InProcessRunner.js +125 -0
- package/src/lambda/handler-runner/in-process-runner/index.js +1 -0
- package/src/lambda/handler-runner/index.js +1 -0
- package/src/lambda/handler-runner/java-runner/JavaRunner.js +114 -0
- package/src/lambda/handler-runner/java-runner/index.js +1 -0
- package/src/lambda/handler-runner/python-runner/PythonRunner.js +138 -0
- package/src/lambda/handler-runner/python-runner/index.js +1 -0
- package/{dist → src}/lambda/handler-runner/python-runner/invoke.py +0 -0
- package/src/lambda/handler-runner/ruby-runner/RubyRunner.js +107 -0
- package/src/lambda/handler-runner/ruby-runner/index.js +1 -0
- package/{dist → src}/lambda/handler-runner/ruby-runner/invoke.rb +0 -0
- package/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js +70 -0
- package/src/lambda/handler-runner/worker-thread-runner/index.js +1 -0
- package/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js +29 -0
- package/src/lambda/index.js +1 -0
- package/src/lambda/routes/index.js +2 -0
- package/src/lambda/routes/invocations/InvocationsController.js +102 -0
- package/src/lambda/routes/invocations/index.js +1 -0
- package/src/lambda/routes/invocations/invocationsRoute.js +77 -0
- package/src/lambda/routes/invoke-async/InvokeAsyncController.js +20 -0
- package/src/lambda/routes/invoke-async/index.js +1 -0
- package/src/lambda/routes/invoke-async/invokeAsyncRoute.js +33 -0
- package/src/utils/__tests__/createUniqueId.test.js +18 -0
- package/src/utils/__tests__/formatToClfTime.test.js +14 -0
- package/src/utils/__tests__/generateHapiPath.test.js +46 -0
- package/src/utils/__tests__/lowerCaseKeys.test.js +30 -0
- package/src/utils/__tests__/parseHeaders.test.js +13 -0
- package/src/utils/__tests__/parseMultiValueHeaders.test.js +24 -0
- package/src/utils/__tests__/parseMultiValueQueryStringParameters.test.js +159 -0
- package/src/utils/__tests__/parseQueryStringParameters.test.js +15 -0
- package/src/utils/__tests__/splitHandlerPathAndName.test.js +54 -0
- package/src/utils/__tests__/unflatten.test.js +32 -0
- package/src/utils/checkDockerDaemon.js +19 -0
- package/src/utils/checkGoVersion.js +16 -0
- package/src/utils/createApiKey.js +5 -0
- package/src/utils/createUniqueId.js +5 -0
- package/src/utils/detectExecutable.js +11 -0
- package/{dist → src}/utils/formatToClfTime.js +6 -14
- package/src/utils/generateHapiPath.js +26 -0
- package/src/utils/getHttpApiCorsConfig.js +28 -0
- package/src/utils/index.js +42 -0
- package/src/utils/jsonPath.js +13 -0
- package/src/utils/logRoutes.js +64 -0
- package/src/utils/lowerCaseKeys.js +6 -0
- package/src/utils/parseHeaders.js +14 -0
- package/src/utils/parseMultiValueHeaders.js +27 -0
- package/src/utils/parseMultiValueQueryStringParameters.js +31 -0
- package/src/utils/parseQueryStringParameters.js +15 -0
- package/src/utils/resolveJoins.js +29 -0
- package/src/utils/splitHandlerPathAndName.js +31 -0
- package/src/utils/unflatten.js +11 -0
- package/dist/ServerlessOffline.js +0 -507
- package/dist/checkEngine.js +0 -21
- package/dist/config/commandOptions.js +0 -149
- package/dist/config/constants.js +0 -30
- package/dist/config/index.js +0 -55
- package/dist/config/supportedRuntimes.js +0 -40
- package/dist/debugLog.js +0 -10
- package/dist/events/authCanExecuteResource.js +0 -35
- package/dist/events/authFunctionNameExtractor.js +0 -87
- package/dist/events/authMatchPolicyResource.js +0 -62
- package/dist/events/http/Endpoint.js +0 -171
- package/dist/events/http/Http.js +0 -77
- package/dist/events/http/HttpEventDefinition.js +0 -36
- package/dist/events/http/HttpServer.js +0 -1363
- package/dist/events/http/OfflineEndpoint.js +0 -40
- package/dist/events/http/authJWTSettingsExtractor.js +0 -76
- package/dist/events/http/authValidateContext.js +0 -48
- package/dist/events/http/createAuthScheme.js +0 -184
- package/dist/events/http/createJWTAuthScheme.js +0 -155
- package/dist/events/http/index.js +0 -15
- package/dist/events/http/javaHelpers.js +0 -99
- package/dist/events/http/lambda-events/LambdaIntegrationEvent.js +0 -85
- package/dist/events/http/lambda-events/LambdaProxyIntegrationEvent.js +0 -244
- package/dist/events/http/lambda-events/LambdaProxyIntegrationEventV2.js +0 -221
- package/dist/events/http/lambda-events/VelocityContext.js +0 -168
- package/dist/events/http/lambda-events/index.js +0 -39
- package/dist/events/http/lambda-events/renderVelocityTemplateObject.js +0 -108
- package/dist/events/http/payloadSchemaValidator.js +0 -13
- package/dist/events/schedule/Schedule.js +0 -182
- package/dist/events/schedule/ScheduleEvent.js +0 -27
- package/dist/events/schedule/ScheduleEventDefinition.js +0 -36
- package/dist/events/schedule/index.js +0 -15
- package/dist/events/websocket/HttpServer.js +0 -112
- package/dist/events/websocket/WebSocket.js +0 -78
- package/dist/events/websocket/WebSocketClients.js +0 -550
- package/dist/events/websocket/WebSocketEventDefinition.js +0 -32
- package/dist/events/websocket/WebSocketServer.js +0 -140
- package/dist/events/websocket/http-routes/_catchAll/catchAllRoute.js +0 -33
- package/dist/events/websocket/http-routes/_catchAll/index.js +0 -15
- package/dist/events/websocket/http-routes/connections/ConnectionsController.js +0 -45
- package/dist/events/websocket/http-routes/connections/connectionsRoutes.js +0 -95
- package/dist/events/websocket/http-routes/connections/index.js +0 -15
- package/dist/events/websocket/http-routes/index.js +0 -23
- package/dist/events/websocket/index.js +0 -15
- package/dist/events/websocket/lambda-events/WebSocketAuthorizerEvent.js +0 -99
- package/dist/events/websocket/lambda-events/WebSocketConnectEvent.js +0 -101
- package/dist/events/websocket/lambda-events/WebSocketDisconnectEvent.js +0 -47
- package/dist/events/websocket/lambda-events/WebSocketEvent.js +0 -54
- package/dist/events/websocket/lambda-events/WebSocketRequestContext.js +0 -98
- package/dist/events/websocket/lambda-events/index.js +0 -39
- package/dist/index.js +0 -19
- package/dist/lambda/HttpServer.js +0 -122
- package/dist/lambda/Lambda.js +0 -113
- package/dist/lambda/LambdaContext.js +0 -53
- package/dist/lambda/LambdaFunction.js +0 -391
- package/dist/lambda/LambdaFunctionPool.js +0 -127
- package/dist/lambda/handler-runner/HandlerRunner.js +0 -223
- package/dist/lambda/handler-runner/child-process-runner/ChildProcessRunner.js +0 -132
- package/dist/lambda/handler-runner/child-process-runner/childProcessHelper.js +0 -40
- package/dist/lambda/handler-runner/child-process-runner/index.js +0 -15
- package/dist/lambda/handler-runner/docker-runner/DockerContainer.js +0 -517
- package/dist/lambda/handler-runner/docker-runner/DockerImage.js +0 -67
- package/dist/lambda/handler-runner/docker-runner/DockerRunner.js +0 -74
- package/dist/lambda/handler-runner/docker-runner/index.js +0 -15
- package/dist/lambda/handler-runner/go-runner/GoRunner.js +0 -211
- package/dist/lambda/handler-runner/go-runner/index.js +0 -15
- package/dist/lambda/handler-runner/in-process-runner/InProcessRunner.js +0 -234
- package/dist/lambda/handler-runner/in-process-runner/index.js +0 -15
- package/dist/lambda/handler-runner/index.js +0 -15
- package/dist/lambda/handler-runner/java-runner/JavaRunner.js +0 -151
- package/dist/lambda/handler-runner/java-runner/index.js +0 -15
- package/dist/lambda/handler-runner/python-runner/PythonRunner.js +0 -180
- package/dist/lambda/handler-runner/python-runner/index.js +0 -15
- package/dist/lambda/handler-runner/ruby-runner/RubyRunner.js +0 -148
- package/dist/lambda/handler-runner/ruby-runner/index.js +0 -15
- package/dist/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js +0 -94
- package/dist/lambda/handler-runner/worker-thread-runner/index.js +0 -15
- package/dist/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js +0 -30
- package/dist/lambda/index.js +0 -15
- package/dist/lambda/routes/index.js +0 -23
- package/dist/lambda/routes/invocations/InvocationsController.js +0 -142
- package/dist/lambda/routes/invocations/index.js +0 -15
- package/dist/lambda/routes/invocations/invocationsRoute.js +0 -90
- package/dist/lambda/routes/invoke-async/InvokeAsyncController.js +0 -38
- package/dist/lambda/routes/invoke-async/index.js +0 -15
- package/dist/lambda/routes/invoke-async/invokeAsyncRoute.js +0 -43
- package/dist/main.js +0 -11
- package/dist/serverlessLog.js +0 -91
- package/dist/utils/checkDockerDaemon.js +0 -27
- package/dist/utils/checkGoVersion.js +0 -27
- package/dist/utils/createApiKey.js +0 -12
- package/dist/utils/createUniqueId.js +0 -14
- package/dist/utils/detectExecutable.js +0 -21
- package/dist/utils/generateHapiPath.js +0 -28
- package/dist/utils/getHttpApiCorsConfig.js +0 -44
- package/dist/utils/index.js +0 -158
- package/dist/utils/jsonPath.js +0 -21
- package/dist/utils/parseHeaders.js +0 -23
- package/dist/utils/parseMultiValueHeaders.js +0 -36
- package/dist/utils/parseMultiValueQueryStringParameters.js +0 -40
- package/dist/utils/parseQueryStringParameters.js +0 -26
- package/dist/utils/resolveJoins.js +0 -34
- package/dist/utils/satisfiesVersionRange.js +0 -20
- package/dist/utils/splitHandlerPathAndName.js +0 -41
- package/dist/utils/unflatten.js +0 -18
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import process, { env, exit } from 'node:process'
|
|
2
|
+
import { log } from '@serverless/utils/log.js'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import {
|
|
5
|
+
commandOptions,
|
|
6
|
+
CUSTOM_OPTION,
|
|
7
|
+
defaultOptions,
|
|
8
|
+
SERVER_SHUTDOWN_TIMEOUT,
|
|
9
|
+
} from './config/index.js'
|
|
10
|
+
|
|
11
|
+
export default class ServerlessOffline {
|
|
12
|
+
#cliOptions = null
|
|
13
|
+
|
|
14
|
+
#http = null
|
|
15
|
+
|
|
16
|
+
#lambda = null
|
|
17
|
+
|
|
18
|
+
#options = null
|
|
19
|
+
|
|
20
|
+
#schedule = null
|
|
21
|
+
|
|
22
|
+
#serverless = null
|
|
23
|
+
|
|
24
|
+
#webSocket = null
|
|
25
|
+
|
|
26
|
+
constructor(serverless, cliOptions) {
|
|
27
|
+
this.#cliOptions = cliOptions
|
|
28
|
+
this.#serverless = serverless
|
|
29
|
+
|
|
30
|
+
this.commands = {
|
|
31
|
+
offline: {
|
|
32
|
+
// add start nested options
|
|
33
|
+
commands: {
|
|
34
|
+
functionsUpdated: {
|
|
35
|
+
lifecycleEvents: ['cleanup'],
|
|
36
|
+
type: 'entrypoint',
|
|
37
|
+
},
|
|
38
|
+
start: {
|
|
39
|
+
lifecycleEvents: ['init', 'ready', 'end'],
|
|
40
|
+
options: commandOptions,
|
|
41
|
+
usage:
|
|
42
|
+
'Simulates API Gateway to call your lambda functions offline using backward compatible initialization.',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
lifecycleEvents: ['start'],
|
|
46
|
+
options: commandOptions,
|
|
47
|
+
usage: 'Simulates API Gateway to call your lambda functions offline.',
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.hooks = {
|
|
52
|
+
'offline:functionsUpdated:cleanup': this.#cleanupFunctions.bind(this),
|
|
53
|
+
'offline:start': this.#startWithExplicitEnd.bind(this),
|
|
54
|
+
'offline:start:end': this.end.bind(this),
|
|
55
|
+
'offline:start:init': this.start.bind(this),
|
|
56
|
+
'offline:start:ready': this.#ready.bind(this),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Entry point for the plugin (sls offline) when running 'sls offline start'
|
|
61
|
+
async start() {
|
|
62
|
+
// Put here so available everywhere, not just in handlers
|
|
63
|
+
env.IS_OFFLINE = true
|
|
64
|
+
|
|
65
|
+
this.#mergeOptions()
|
|
66
|
+
|
|
67
|
+
const { httpEvents, lambdas, scheduleEvents, webSocketEvents } =
|
|
68
|
+
this.#getEvents()
|
|
69
|
+
|
|
70
|
+
// if (lambdas.length > 0) {
|
|
71
|
+
await this.#createLambda(lambdas)
|
|
72
|
+
// }
|
|
73
|
+
|
|
74
|
+
const eventModules = []
|
|
75
|
+
|
|
76
|
+
if (httpEvents.length > 0) {
|
|
77
|
+
eventModules.push(this.#createHttp(httpEvents))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!this.#options.disableScheduledEvents && scheduleEvents.length > 0) {
|
|
81
|
+
eventModules.push(this.#createSchedule(scheduleEvents))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (webSocketEvents.length > 0) {
|
|
85
|
+
eventModules.push(this.#createWebSocket(webSocketEvents))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await Promise.all(eventModules)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async #ready() {
|
|
92
|
+
await this.#listenForTermination()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async end(skipExit) {
|
|
96
|
+
log.info('Halting offline server')
|
|
97
|
+
|
|
98
|
+
const eventModules = []
|
|
99
|
+
|
|
100
|
+
if (this.#lambda) {
|
|
101
|
+
eventModules.push(this.#lambda.cleanup())
|
|
102
|
+
eventModules.push(this.#lambda.stop(SERVER_SHUTDOWN_TIMEOUT))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (this.#http) {
|
|
106
|
+
eventModules.push(this.#http.stop(SERVER_SHUTDOWN_TIMEOUT))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// if (this.#schedule) {
|
|
110
|
+
// eventModules.push(this.#schedule.stop())
|
|
111
|
+
// }
|
|
112
|
+
|
|
113
|
+
if (this.#webSocket) {
|
|
114
|
+
eventModules.push(this.#webSocket.stop(SERVER_SHUTDOWN_TIMEOUT))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await Promise.all(eventModules)
|
|
118
|
+
|
|
119
|
+
if (!skipExit) {
|
|
120
|
+
exit(0)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async #cleanupFunctions() {
|
|
125
|
+
if (this.#lambda) {
|
|
126
|
+
log.debug('Forcing cleanup of Lambda functions')
|
|
127
|
+
await this.#lambda.cleanup()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Entry point for the plugin (serverless offline) when running 'serverless offline'
|
|
133
|
+
* The call to this.end() would terminate the process before 'offline:start:end' could be consumed
|
|
134
|
+
* by downstream plugins. When running serverless offline that can be expected, but docs say that
|
|
135
|
+
* 'serverless offline start' will provide the init and end hooks for other plugins to consume
|
|
136
|
+
* */
|
|
137
|
+
async #startWithExplicitEnd() {
|
|
138
|
+
await this.start()
|
|
139
|
+
await this.#ready()
|
|
140
|
+
this.end()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async #listenForTermination() {
|
|
144
|
+
const command = await new Promise((resolve) => {
|
|
145
|
+
process
|
|
146
|
+
// SIGINT will be usually sent when user presses ctrl+c
|
|
147
|
+
.on('SIGINT', () => resolve('SIGINT'))
|
|
148
|
+
// SIGTERM is a default termination signal in many cases,
|
|
149
|
+
// for example when "killing" a subprocess spawned in node
|
|
150
|
+
// with child_process methods
|
|
151
|
+
.on('SIGTERM', () => resolve('SIGTERM'))
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
log.info(`Got ${command} signal. Offline Halting...`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async #createLambda(lambdas, skipStart) {
|
|
158
|
+
const { default: Lambda } = await import('./lambda/index.js')
|
|
159
|
+
|
|
160
|
+
this.#lambda = new Lambda(this.#serverless, this.#options)
|
|
161
|
+
|
|
162
|
+
this.#lambda.create(lambdas)
|
|
163
|
+
|
|
164
|
+
if (!skipStart) {
|
|
165
|
+
await this.#lambda.start()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async #createHttp(events, skipStart) {
|
|
170
|
+
const { default: Http } = await import('./events/http/index.js')
|
|
171
|
+
|
|
172
|
+
this.#http = new Http(this.#serverless, this.#options, this.#lambda)
|
|
173
|
+
|
|
174
|
+
await this.#http.registerPlugins()
|
|
175
|
+
|
|
176
|
+
this.#http.create(events)
|
|
177
|
+
|
|
178
|
+
// HTTP Proxy defined in Resource
|
|
179
|
+
this.#http.createResourceRoutes()
|
|
180
|
+
|
|
181
|
+
// Not found handling
|
|
182
|
+
// we have to create the 404 routes last, otherwise we could have
|
|
183
|
+
// collisions with catch all routes, e.g. any (proxy+}
|
|
184
|
+
this.#http.create404Route()
|
|
185
|
+
|
|
186
|
+
if (!skipStart) {
|
|
187
|
+
await this.#http.start()
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async #createSchedule(events) {
|
|
192
|
+
const { default: Schedule } = await import('./events/schedule/index.js')
|
|
193
|
+
|
|
194
|
+
this.#schedule = new Schedule(
|
|
195
|
+
this.#lambda,
|
|
196
|
+
this.#serverless.service.provider.region,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
this.#schedule.create(events)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async #createWebSocket(events) {
|
|
203
|
+
const { default: WebSocket } = await import('./events/websocket/index.js')
|
|
204
|
+
|
|
205
|
+
this.#webSocket = new WebSocket(
|
|
206
|
+
this.#serverless,
|
|
207
|
+
this.#options,
|
|
208
|
+
this.#lambda,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
this.#webSocket.create(events)
|
|
212
|
+
|
|
213
|
+
return this.#webSocket.start()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#mergeOptions() {
|
|
217
|
+
const {
|
|
218
|
+
service: { custom = {}, provider },
|
|
219
|
+
} = this.#serverless
|
|
220
|
+
|
|
221
|
+
const customOptions = custom[CUSTOM_OPTION]
|
|
222
|
+
|
|
223
|
+
// merge options
|
|
224
|
+
// order of Precedence: command line options, custom options, defaults.
|
|
225
|
+
this.#options = {
|
|
226
|
+
...defaultOptions,
|
|
227
|
+
...customOptions,
|
|
228
|
+
...this.#cliOptions,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Parse CORS options
|
|
232
|
+
this.#options.corsAllowHeaders = this.#options.corsAllowHeaders
|
|
233
|
+
.replace(/\s/g, '')
|
|
234
|
+
.split(',')
|
|
235
|
+
this.#options.corsAllowOrigin = this.#options.corsAllowOrigin
|
|
236
|
+
.replace(/\s/g, '')
|
|
237
|
+
.split(',')
|
|
238
|
+
this.#options.corsExposedHeaders = this.#options.corsExposedHeaders
|
|
239
|
+
.replace(/\s/g, '')
|
|
240
|
+
.split(',')
|
|
241
|
+
|
|
242
|
+
if (this.#options.corsDisallowCredentials) {
|
|
243
|
+
this.#options.corsAllowCredentials = false
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
this.#options.corsConfig = {
|
|
247
|
+
credentials: this.#options.corsAllowCredentials,
|
|
248
|
+
exposedHeaders: this.#options.corsExposedHeaders,
|
|
249
|
+
headers: this.#options.corsAllowHeaders,
|
|
250
|
+
origin: this.#options.corsAllowOrigin,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
log.notice()
|
|
254
|
+
log.notice(
|
|
255
|
+
`Starting Offline at stage ${provider.stage} ${chalk.gray(
|
|
256
|
+
`(${provider.region})`,
|
|
257
|
+
)}`,
|
|
258
|
+
)
|
|
259
|
+
log.notice()
|
|
260
|
+
log.debug('options:', this.#options)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#getEvents() {
|
|
264
|
+
const { service } = this.#serverless
|
|
265
|
+
|
|
266
|
+
const httpEvents = []
|
|
267
|
+
const lambdas = []
|
|
268
|
+
const scheduleEvents = []
|
|
269
|
+
const webSocketEvents = []
|
|
270
|
+
|
|
271
|
+
const functionKeys = service.getAllFunctions()
|
|
272
|
+
|
|
273
|
+
let hasPrivateHttpEvent = false
|
|
274
|
+
|
|
275
|
+
functionKeys.forEach((functionKey) => {
|
|
276
|
+
const functionDefinition = service.getFunction(functionKey)
|
|
277
|
+
|
|
278
|
+
lambdas.push({ functionDefinition, functionKey })
|
|
279
|
+
|
|
280
|
+
const events = service.getAllEventsInFunction(functionKey) || []
|
|
281
|
+
|
|
282
|
+
events.forEach((event) => {
|
|
283
|
+
const { http, httpApi, schedule, websocket } = event
|
|
284
|
+
|
|
285
|
+
if ((http || httpApi) && functionDefinition.handler) {
|
|
286
|
+
const httpEvent = {
|
|
287
|
+
functionKey,
|
|
288
|
+
handler: functionDefinition.handler,
|
|
289
|
+
http: http || httpApi,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (httpApi) {
|
|
293
|
+
// Ensure definitions for 'httpApi' events are objects so that they can be marked
|
|
294
|
+
// with an 'isHttpApi' property (they are handled differently to 'http' events)
|
|
295
|
+
if (typeof httpEvent.http === 'string') {
|
|
296
|
+
httpEvent.http = {
|
|
297
|
+
routeKey: httpEvent.http === '*' ? '$default' : httpEvent.http,
|
|
298
|
+
}
|
|
299
|
+
} else if (typeof httpEvent.http === 'object') {
|
|
300
|
+
if (!httpEvent.http.method) {
|
|
301
|
+
log.warning(
|
|
302
|
+
`Event definition is missing a method for function "${functionKey}"`,
|
|
303
|
+
)
|
|
304
|
+
httpEvent.http.method = ''
|
|
305
|
+
}
|
|
306
|
+
if (
|
|
307
|
+
httpEvent.http.method === '*' &&
|
|
308
|
+
httpEvent.http.path === '*'
|
|
309
|
+
) {
|
|
310
|
+
httpEvent.http.routeKey = '$default'
|
|
311
|
+
} else {
|
|
312
|
+
const resolvedMethod =
|
|
313
|
+
httpEvent.http.method === '*'
|
|
314
|
+
? 'ANY'
|
|
315
|
+
: httpEvent.http.method.toUpperCase()
|
|
316
|
+
httpEvent.http.routeKey = `${resolvedMethod} ${httpEvent.http.path}`
|
|
317
|
+
}
|
|
318
|
+
// Clear these properties to avoid confusion (they will be derived from the routeKey
|
|
319
|
+
// when needed later)
|
|
320
|
+
delete httpEvent.http.method
|
|
321
|
+
delete httpEvent.http.path
|
|
322
|
+
} else {
|
|
323
|
+
log.warning(
|
|
324
|
+
`Event definition must be a string or object but received ${typeof httpEvent.http} for function "${functionKey}"`,
|
|
325
|
+
)
|
|
326
|
+
httpEvent.http.routeKey = ''
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
httpEvent.http.isHttpApi = true
|
|
330
|
+
if (
|
|
331
|
+
functionDefinition.httpApi &&
|
|
332
|
+
functionDefinition.httpApi.payload
|
|
333
|
+
) {
|
|
334
|
+
httpEvent.http.payload = functionDefinition.httpApi.payload
|
|
335
|
+
} else {
|
|
336
|
+
httpEvent.http.payload =
|
|
337
|
+
service.provider.httpApi && service.provider.httpApi.payload
|
|
338
|
+
? service.provider.httpApi.payload
|
|
339
|
+
: '2.0'
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (http && http.private) {
|
|
344
|
+
hasPrivateHttpEvent = true
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
httpEvents.push(httpEvent)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (schedule) {
|
|
351
|
+
scheduleEvents.push({
|
|
352
|
+
functionKey,
|
|
353
|
+
schedule,
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (websocket) {
|
|
358
|
+
webSocketEvents.push({
|
|
359
|
+
functionKey,
|
|
360
|
+
websocket,
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
// for simple API Key authentication model
|
|
367
|
+
if (hasPrivateHttpEvent) {
|
|
368
|
+
log.notice(`Key with token: ${this.#options.apiKey}`)
|
|
369
|
+
|
|
370
|
+
if (this.#options.noAuth) {
|
|
371
|
+
log.notice(
|
|
372
|
+
'Authorizers are turned off. You do not need to use x-api-key header.',
|
|
373
|
+
)
|
|
374
|
+
} else {
|
|
375
|
+
log.notice('Remember to use x-api-key on the request headers')
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
httpEvents,
|
|
381
|
+
lambdas,
|
|
382
|
+
scheduleEvents,
|
|
383
|
+
webSocketEvents,
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// TODO FIXME
|
|
388
|
+
// TEMP quick fix to expose for testing, look for better solution
|
|
389
|
+
internals() {
|
|
390
|
+
return {
|
|
391
|
+
createHttp: (events, skipStart) => {
|
|
392
|
+
return this.#createHttp(events, skipStart)
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
createLambda: (lambdas, skipStart) => {
|
|
396
|
+
return this.#createLambda(lambdas, skipStart)
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
getApiGatewayServer: () => {
|
|
400
|
+
return this.#http.getServer()
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
getEvents: () => {
|
|
404
|
+
return this.#getEvents()
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
mergeOptions: () => {
|
|
408
|
+
this.#mergeOptions()
|
|
409
|
+
},
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
apiKey: {
|
|
3
|
+
type: 'string',
|
|
4
|
+
usage:
|
|
5
|
+
'Defines the API key value to be used for endpoints marked as private. Defaults to a random hash.',
|
|
6
|
+
},
|
|
7
|
+
corsAllowHeaders: {
|
|
8
|
+
type: 'string',
|
|
9
|
+
usage:
|
|
10
|
+
'Used to build the Access-Control-Allow-Headers header for CORS support.',
|
|
11
|
+
},
|
|
12
|
+
corsAllowOrigin: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
usage:
|
|
15
|
+
'Used to build the Access-Control-Allow-Origin header for CORS support.',
|
|
16
|
+
},
|
|
17
|
+
corsDisallowCredentials: {
|
|
18
|
+
type: 'boolean',
|
|
19
|
+
usage:
|
|
20
|
+
'Used to override the Access-Control-Allow-Credentials default (which is true) to false.',
|
|
21
|
+
},
|
|
22
|
+
corsExposedHeaders: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
usage:
|
|
25
|
+
'Used to build the Access-Control-Exposed-Headers response header for CORS support',
|
|
26
|
+
},
|
|
27
|
+
disableCookieValidation: {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
usage: 'Used to disable cookie-validation on hapi.js-server',
|
|
30
|
+
},
|
|
31
|
+
disableScheduledEvents: {
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
usage:
|
|
34
|
+
'Disables all scheduled events. Overrides configurations in serverless.yml. Default: false',
|
|
35
|
+
},
|
|
36
|
+
dockerHost: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
usage: 'The host name of Docker. Default: localhost',
|
|
39
|
+
},
|
|
40
|
+
dockerHostServicePath: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
usage:
|
|
43
|
+
'Defines service path which is used by SLS running inside Docker container',
|
|
44
|
+
},
|
|
45
|
+
dockerNetwork: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
usage: 'The network that the Docker container will connect to',
|
|
48
|
+
},
|
|
49
|
+
dockerReadOnly: {
|
|
50
|
+
type: 'boolean',
|
|
51
|
+
usage: 'Marks if the docker code layer should be read only. Default: true',
|
|
52
|
+
},
|
|
53
|
+
enforceSecureCookies: {
|
|
54
|
+
type: 'boolean',
|
|
55
|
+
usage: 'Enforce secure cookies',
|
|
56
|
+
},
|
|
57
|
+
functionCleanupIdleTimeSeconds: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
usage: 'Number of seconds until an idle function is eligible for cleanup',
|
|
60
|
+
},
|
|
61
|
+
hideStackTraces: {
|
|
62
|
+
type: 'boolean',
|
|
63
|
+
usage: 'Hide the stack trace on lambda failure. Default: false',
|
|
64
|
+
},
|
|
65
|
+
host: {
|
|
66
|
+
shortcut: 'o',
|
|
67
|
+
type: 'string',
|
|
68
|
+
usage: 'The host name to listen on. Default: localhost',
|
|
69
|
+
},
|
|
70
|
+
httpPort: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
usage: 'HTTP port to listen on. Default: 3000',
|
|
73
|
+
},
|
|
74
|
+
httpsProtocol: {
|
|
75
|
+
shortcut: 'H',
|
|
76
|
+
type: 'string',
|
|
77
|
+
usage:
|
|
78
|
+
'To enable HTTPS, specify directory (relative to your cwd, typically your project dir) for both cert.pem and key.pem files.',
|
|
79
|
+
},
|
|
80
|
+
ignoreJWTSignature: {
|
|
81
|
+
type: 'boolean',
|
|
82
|
+
usage:
|
|
83
|
+
"When using HttpApi with a JWT authorizer, don't check the signature of the JWT token. This should only be used for local development.",
|
|
84
|
+
},
|
|
85
|
+
lambdaPort: {
|
|
86
|
+
type: 'string',
|
|
87
|
+
usage: 'Lambda http port to listen on. Default: 3002',
|
|
88
|
+
},
|
|
89
|
+
layersDir: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
usage:
|
|
92
|
+
'The directory layers should be stored in. Default: {codeDir}/.serverless-offline/layers',
|
|
93
|
+
},
|
|
94
|
+
noAuth: {
|
|
95
|
+
type: 'boolean',
|
|
96
|
+
usage: 'Turns off all authorizers',
|
|
97
|
+
},
|
|
98
|
+
noPrependStageInUrl: {
|
|
99
|
+
type: 'boolean',
|
|
100
|
+
usage: "Don't prepend http routes with the stage.",
|
|
101
|
+
},
|
|
102
|
+
noStripTrailingSlashInUrl: {
|
|
103
|
+
type: 'boolean',
|
|
104
|
+
usage: "Don't strip trailing slash from http routes.",
|
|
105
|
+
},
|
|
106
|
+
noTimeout: {
|
|
107
|
+
shortcut: 't',
|
|
108
|
+
type: 'boolean',
|
|
109
|
+
usage: 'Disables the timeout feature.',
|
|
110
|
+
},
|
|
111
|
+
prefix: {
|
|
112
|
+
shortcut: 'p',
|
|
113
|
+
type: 'string',
|
|
114
|
+
usage:
|
|
115
|
+
'Adds a prefix to every path, to send your requests to http://localhost:3000/prefix/[your_path] instead.',
|
|
116
|
+
},
|
|
117
|
+
printOutput: {
|
|
118
|
+
type: 'boolean',
|
|
119
|
+
usage: 'Outputs your lambda response to the terminal.',
|
|
120
|
+
},
|
|
121
|
+
reloadHandler: {
|
|
122
|
+
type: 'boolean',
|
|
123
|
+
usage: 'Reloads handler with each request.',
|
|
124
|
+
},
|
|
125
|
+
resourceRoutes: {
|
|
126
|
+
type: 'boolean',
|
|
127
|
+
usage: 'Turns on loading of your HTTP proxy settings from serverless.yml.',
|
|
128
|
+
},
|
|
129
|
+
useChildProcesses: {
|
|
130
|
+
type: 'boolean',
|
|
131
|
+
usage: 'Use separate node process to run handlers',
|
|
132
|
+
},
|
|
133
|
+
useDocker: {
|
|
134
|
+
type: 'boolean',
|
|
135
|
+
usage: 'Uses docker for node/python/ruby/provided',
|
|
136
|
+
},
|
|
137
|
+
useInProcess: {
|
|
138
|
+
type: 'boolean',
|
|
139
|
+
usage: "Run handlers in the same process as 'serverless-offline'",
|
|
140
|
+
},
|
|
141
|
+
webSocketHardTimeout: {
|
|
142
|
+
type: 'string',
|
|
143
|
+
usage:
|
|
144
|
+
'Set WebSocket hard timeout in seconds to reproduce AWS limits (https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html#apigateway-execution-service-websocket-limits-table). Default: 7200 (2 hours)',
|
|
145
|
+
},
|
|
146
|
+
webSocketIdleTimeout: {
|
|
147
|
+
type: 'string',
|
|
148
|
+
usage:
|
|
149
|
+
'Set WebSocket idle timeout in seconds to reproduce AWS limits (https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html#apigateway-execution-service-websocket-limits-table). Default: 600 (10 minutes)',
|
|
150
|
+
},
|
|
151
|
+
websocketPort: {
|
|
152
|
+
type: 'string',
|
|
153
|
+
usage: 'Websocket port to listen on. Default: 3001',
|
|
154
|
+
},
|
|
155
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// dummy placeholder url for the WHATWG URL constructor
|
|
2
|
+
// https://github.com/nodejs/node/issues/12682
|
|
3
|
+
export const BASE_URL_PLACEHOLDER = 'http://example'
|
|
4
|
+
|
|
5
|
+
export const CUSTOM_OPTION = 'serverless-offline'
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_LAMBDA_RUNTIME = 'nodejs12.x'
|
|
8
|
+
|
|
9
|
+
// https://docs.aws.amazon.com/lambda/latest/dg/limits.html
|
|
10
|
+
export const DEFAULT_LAMBDA_MEMORY_SIZE = 1024
|
|
11
|
+
// default function timeout in seconds
|
|
12
|
+
export const DEFAULT_LAMBDA_TIMEOUT = 900 // 15 min
|
|
13
|
+
|
|
14
|
+
// timeout for all connections to be closed
|
|
15
|
+
export const SERVER_SHUTDOWN_TIMEOUT = 5000
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_WEBSOCKETS_API_ROUTE_SELECTION_EXPRESSION =
|
|
18
|
+
'$request.body.action'
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_WEBSOCKETS_ROUTE = '$default'
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_DOCKER_CONTAINER_PORT = 9001
|
|
@@ -1,17 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import { createApiKey } from '../utils/index.js'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
exports.default = void 0;
|
|
7
|
-
|
|
8
|
-
var _index = require("../utils/index.js");
|
|
9
|
-
|
|
10
|
-
var _default = {
|
|
11
|
-
allowCache: false,
|
|
12
|
-
apiKey: (0, _index.createApiKey)(),
|
|
13
|
-
corsAllowCredentials: true,
|
|
14
|
-
// TODO no CLI option
|
|
3
|
+
export default {
|
|
4
|
+
apiKey: createApiKey(),
|
|
5
|
+
corsAllowCredentials: true, // TODO no CLI option
|
|
15
6
|
corsAllowHeaders: 'accept,content-type,x-api-key,authorization',
|
|
16
7
|
corsAllowOrigin: '*',
|
|
17
8
|
corsExposedHeaders: 'WWW-Authenticate,Server-Authorization',
|
|
@@ -35,12 +26,12 @@ var _default = {
|
|
|
35
26
|
noTimeout: false,
|
|
36
27
|
prefix: '',
|
|
37
28
|
printOutput: false,
|
|
29
|
+
reloadHandler: false,
|
|
38
30
|
resourceRoutes: false,
|
|
39
31
|
useChildProcesses: false,
|
|
40
32
|
useDocker: false,
|
|
41
|
-
|
|
33
|
+
useInProcess: false,
|
|
42
34
|
webSocketHardTimeout: 7200,
|
|
43
35
|
webSocketIdleTimeout: 600,
|
|
44
|
-
websocketPort: 3001
|
|
45
|
-
}
|
|
46
|
-
exports.default = _default;
|
|
36
|
+
websocketPort: 3001,
|
|
37
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// native runtime support for AWS
|
|
2
|
+
// https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html
|
|
3
|
+
|
|
4
|
+
// .NET CORE
|
|
5
|
+
export const supportedDotnetcore = new Set([
|
|
6
|
+
// 'dotnet6',
|
|
7
|
+
// 'dotnetcore3.1',
|
|
8
|
+
])
|
|
9
|
+
|
|
10
|
+
// GO
|
|
11
|
+
export const supportedGo = new Set(['go1.x'])
|
|
12
|
+
|
|
13
|
+
// JAVA
|
|
14
|
+
export const supportedJava = new Set(['java8', 'java8.al2', 'java11'])
|
|
15
|
+
|
|
16
|
+
// NODE.JS
|
|
17
|
+
export const supportedNodejs = new Set([
|
|
18
|
+
'nodejs12.x',
|
|
19
|
+
'nodejs14.x',
|
|
20
|
+
'nodejs16.x',
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
// PROVIDED
|
|
24
|
+
export const supportedProvided = new Set(['provided', 'provided.al2'])
|
|
25
|
+
|
|
26
|
+
// PYTHON
|
|
27
|
+
export const supportedPython = new Set([
|
|
28
|
+
'python3.6',
|
|
29
|
+
'python3.7',
|
|
30
|
+
'python3.8',
|
|
31
|
+
'python3.9',
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
// RUBY
|
|
35
|
+
export const supportedRuby = new Set(['ruby2.7'])
|
|
36
|
+
|
|
37
|
+
// deprecated runtimes
|
|
38
|
+
// https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html
|
|
39
|
+
export const supportedRuntimes = new Set([
|
|
40
|
+
...supportedDotnetcore,
|
|
41
|
+
...supportedGo,
|
|
42
|
+
...supportedJava,
|
|
43
|
+
...supportedNodejs,
|
|
44
|
+
...supportedProvided,
|
|
45
|
+
...supportedPython,
|
|
46
|
+
...supportedRuby,
|
|
47
|
+
])
|