/
index.js
233 lines (193 loc) Β· 7.63 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
/*
@TODO:
- handle package.individually?
https://github.com/serverless/serverless/blob/master/lib/plugins/package/lib/packageService.js#L37
- support for enabling chrome only on specific functions?
- instead of including fs-p dep, use the fs methods from the Utils class provided by Serverless
or use fs-extra directly.
- config option to, instead of including chrome in artifact zip, download it on
cold-start invocations this could be useful for development, instead of having
to upload 50MB each deploy
- tests.
- custom.chrome.functions breaks when a wrapped and non-wrapped function have the
same handler.js file
*/
import * as path from 'path'
import * as fs from 'fs-p' // deprecated. use fs-extra?
import globby from 'globby'
import { SERVERLESS_FOLDER, BUILD_FOLDER, INCLUDES } from './constants'
import {
throwIfUnsupportedProvider,
throwIfUnsupportedRuntime,
throwIfWrongPluginOrder,
getHandlerFileAndExportName,
} from './utils'
const wrapperTemplateMap = {
'aws-nodejs6.10': 'wrapper-aws-nodejs.js',
'aws-nodejs8.10': 'wrapper-aws-nodejs.js',
'aws-nodejs10.x': 'wrapper-aws-nodejs.js',
'aws-nodejs12.x': 'wrapper-aws-nodejs.js',
}
export default class ServerlessChrome {
constructor (serverless, options) {
this.serverless = serverless
this.options = options
const {
provider: { name: providerName, runtime },
plugins,
} = serverless.service
throwIfUnsupportedProvider(providerName)
throwIfUnsupportedRuntime(runtime)
throwIfWrongPluginOrder(plugins)
this.hooks = {
'before:offline:start:init': this.beforeCreateDeploymentArtifacts.bind(this),
'before:package:createDeploymentArtifacts': this.beforeCreateDeploymentArtifacts.bind(this),
'after:package:createDeploymentArtifacts': this.afterCreateDeploymentArtifacts.bind(this),
'before:invoke:local:invoke': this.beforeCreateDeploymentArtifacts.bind(this),
'after:invoke:local:invoke': this.cleanup.bind(this),
'before:webpack:package:packExternalModules': this.webpackPackageBinaries.bind(this),
}
// only mess with the service path if we're not already known to be within a .build folder
this.messWithServicePath = !plugins.includes('serverless-plugin-typescript')
// annoyingly we have to do stuff differently if using serverless-webpack plugin. lame.
this.webpack = plugins.includes('serverless-webpack')
}
async webpackPackageBinaries () {
const { config: { servicePath }, service } = this.serverless
const packagedIdividually = service.package && service.package.individually
if (packagedIdividually) {
const functionsToCopyTo =
(service.custom && service.custom.chrome && service.custom.chrome.functions) ||
service.getAllFunctions()
await Promise.all(functionsToCopyTo.map(async (functionName) => {
await fs.copy(
path.join(servicePath, 'node_modules/@serverless-chrome/lambda/dist/headless-chromium'),
path.resolve(servicePath, `.webpack/${functionName}/headless-chromium`)
)
}))
} else {
await fs.copy(
path.join(servicePath, 'node_modules/@serverless-chrome/lambda/dist/headless-chromium'),
path.resolve(servicePath, '.webpack/service/headless-chromium')
)
}
}
async beforeCreateDeploymentArtifacts () {
const {
config,
cli,
utils,
service,
service: {
provider: { name: providerName, runtime },
},
} = this.serverless
const functionsToWrap =
(service.custom &&
service.custom.chrome &&
service.custom.chrome.functions) ||
service.getAllFunctions()
service.package.include = service.package.include || []
cli.log('Injecting Headless Chrome...')
// Save original service path and functions
this.originalServicePath = config.servicePath
// Fake service path so that serverless will know what to zip
// Unless, we're already in a .build folder from another plugin
if (this.messWithServicePath) {
config.servicePath = path.join(this.originalServicePath, BUILD_FOLDER)
if (!fs.existsSync(config.servicePath)) {
fs.mkdirpSync(config.servicePath)
}
// include node_modules into build
if (
!fs.existsSync(path.resolve(path.join(BUILD_FOLDER, 'node_modules')))
) {
fs.symlinkSync(
path.resolve('node_modules'),
path.resolve(path.join(BUILD_FOLDER, 'node_modules')),
'junction'
)
}
// include any "extras" from the "include" section
const files = await globby(
[...service.package.include, '**', '!node_modules/**'],
{
cwd: this.originalServicePath,
}
)
files.forEach((filename) => {
const sourceFile = path.resolve(path.join(this.originalServicePath, filename))
const destFileName = path.resolve(path.join(config.servicePath, filename))
const dirname = path.dirname(destFileName)
if (!fs.existsSync(dirname)) {
fs.mkdirpSync(dirname)
}
if (!fs.existsSync(destFileName)) {
fs.copySync(sourceFile, destFileName)
}
})
}
// Add our node_modules dependencies to the package includes
service.package.include = [...service.package.include, ...INCLUDES]
await Promise.all(functionsToWrap.map(async (functionName) => {
const { handler } = service.getFunction(functionName)
const { filePath, fileName, exportName } = getHandlerFileAndExportName(handler)
const handlerCodePath = path.join(config.servicePath, filePath)
const originalFileRenamed = `${utils.generateShortId()}___${fileName}`
const customPluginOptions =
(service.custom && service.custom.chrome) || {}
const launcherOptions = {
chromePath: (this.webpack && !process.env.IS_LOCAL) ? '/var/task/headless-chromium' : undefined,
...customPluginOptions,
flags: customPluginOptions.flags || [],
}
// Read in the wrapper handler code template
const wrapperTemplate = await utils.readFile(path.resolve(
__dirname,
'..',
'src',
wrapperTemplateMap[`${providerName}-${runtime}`]
))
// Include the original handler via require
const wrapperCode = wrapperTemplate
.replace(
"'REPLACE_WITH_HANDLER_REQUIRE'",
`require('./${originalFileRenamed}')`
)
.replace("'REPLACE_WITH_OPTIONS'", JSON.stringify(launcherOptions))
.replace(/REPLACE_WITH_EXPORT_NAME/gm, exportName)
// Move the original handler's file aside
await fs.move(
path.resolve(handlerCodePath, fileName),
path.resolve(handlerCodePath, originalFileRenamed)
)
// Write the wrapper code to the function's handler path
await utils.writeFile(
path.resolve(handlerCodePath, fileName),
wrapperCode
)
}))
}
async afterCreateDeploymentArtifacts () {
if (this.messWithServicePath) {
// Copy .build to .serverless
await fs.copy(
path.join(this.originalServicePath, BUILD_FOLDER, SERVERLESS_FOLDER),
path.join(this.originalServicePath, SERVERLESS_FOLDER)
)
// this.serverless.service.package.artifact = path.join(
// this.originalServicePath,
// SERVERLESS_FOLDER
// path.basename(this.serverless.service.package.artifact)
// )
// Cleanup after everything is copied
await this.cleanup()
}
}
async cleanup () {
// Restore service path
this.serverless.config.servicePath = this.originalServicePath
// Remove temp build folder
fs.removeSync(path.join(this.originalServicePath, BUILD_FOLDER))
}
}