Skip to content

Instantly share code, notes, and snippets.

@enapupe
Last active June 8, 2022 14:31
Show Gist options
  • Save enapupe/6ac7cc11aba1e493d0eca9a56a73a9c8 to your computer and use it in GitHub Desktop.
Save enapupe/6ac7cc11aba1e493d0eca9a56a73a9c8 to your computer and use it in GitHub Desktop.
serverless plugin that will check required environment variables against provided environment variables
'use strict'
const fs = require('fs')
const path = require('path')
const { sync: resolveSync } = require('resolve')
const acornLoose = require('acorn-loose')
const walk = require('acorn-walk')
const resolveImportSourcePathSync = (filePath, importSource) => {
return resolveSync(importSource, {
basedir: path.dirname(filePath),
})
}
const getImportSourcesAndEnvs = (path) => {
const data = fs.readFileSync(path).toString()
const parsed = acornLoose.parse(data, {
ecmaVersion: 2020,
})
const required = []
const envs = []
walk.simple(parsed, {
VariableDeclaration: (node) => {
node.declarations.forEach((declaration) => {
if (declaration.init && declaration.init.type === 'CallExpression') {
declaration?.init?.callee?.expressions?.forEach((exp) => {
if (
exp.type === 'MemberExpression' &&
// the transpilled name of the function:
exp.object.name === 'get_env_1' &&
// the second argument is not defined,
// meaning the env IS required:
typeof declaration.init.arguments[1] === 'undefined'
) {
envs.push(declaration.init.arguments[0].value)
}
})
}
if (declaration.init?.callee?.name === '__importDefault') {
required.push(declaration.init.arguments[0].arguments[0].value)
}
if (
declaration.init &&
declaration.init.type === 'CallExpression' &&
declaration?.init?.callee?.name === 'require'
) {
required.push(declaration.init.arguments[0].value)
}
})
},
})
return { required, envs }
}
const INTERNAL_MODULE_SOURCE = /^\./
const collectEnvs = (entry) => {
const visited = {}
const queue = [entry]
const environs = []
while (queue.length) {
const filePath = queue.shift()
const { required, envs } = getImportSourcesAndEnvs(filePath)
if (!visited[filePath] && envs.length) {
environs.push([filePath, envs])
}
for (let importSource of required) {
if (INTERNAL_MODULE_SOURCE.test(importSource)) {
const resolved = resolveImportSourcePathSync(filePath, importSource)
if (!visited[resolved]) {
queue.push(resolved)
}
}
}
visited[filePath] = true
}
return environs
}
const getRequiredEnvs = (functionHandler) => {
const fileName = `./${functionHandler.replace(/\.\w+$/, '.js')}`
return collectEnvs(fileName)
}
class EnvironChecker {
constructor(serverless, cliOptions, { log }) {
this.log = log
this.serverless = serverless
this.hooks = {
'before:package:initialize': () => this.beforeDeploy(),
}
}
beforeDeploy() {
const { functions, provider } = this.serverless.service
const errors = {}
Object.entries(functions).forEach(([nada, fn]) => {
const requiredEnvs = getRequiredEnvs(fn.handler)
const providedEnvs = Object.keys({
...provider.environment,
...fn.environment,
})
requiredEnvs.forEach(([path, envs]) => {
envs.forEach((env) => {
if (!providedEnvs.includes(env)) {
if (errors[fn.name]) {
errors[fn.name].push([path, env])
} else {
errors[fn.name] = [path, env]
}
}
})
})
})
if (Object.keys(errors).length) {
this.log.error('The following envs are missing:')
this.log.error(errors)
throw new this.serverless.classes.Error('Required envs are missing')
}
}
}
module.exports = EnvironChecker
service: name
provider: [...]
functions: [...]
plugins:
- ./env-check.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment