serverless-offline-edge-lambda
Version:
A plugin for the Serverless Framework that simulates the behavior of AWS CloudFront Edge Lambdas while developing offline.
310 lines • 14.9 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __asyncValues = (this && this.__asyncValues) || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BehaviorRouter = void 0;
const body_parser_1 = __importDefault(require("body-parser"));
const connect_1 = __importDefault(require("connect"));
const cookie_parser_1 = __importDefault(require("cookie-parser"));
const chokidar = __importStar(require("chokidar"));
const fs = __importStar(require("fs-extra"));
const http_1 = require("http");
const http_status_codes_1 = require("http-status-codes");
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const url_1 = require("url");
const debounce_1 = require("./utils/debounce");
const http_2 = require("./errors/http");
const function_set_1 = require("./function-set");
const middlewares_1 = require("./middlewares");
const services_1 = require("./services");
const utils_1 = require("./utils");
class BehaviorRouter {
constructor(serverless, options) {
var _a, _b;
this.serverless = serverless;
this.options = options;
this.behaviors = new Map();
this.started = false;
this.restarting = false;
this.server = null;
this.log = serverless.cli.log.bind(serverless.cli);
this.builder = (0, utils_1.buildConfig)(serverless);
this.context = (0, utils_1.buildContext)();
this.cfResources = ((_b = (_a = serverless.service) === null || _a === void 0 ? void 0 : _a.resources) === null || _b === void 0 ? void 0 : _b.Resources) || {};
this.cacheDir = path.resolve(options.cacheDir || path.join(os.tmpdir(), 'edge-lambda'));
this.fileDir = path.resolve(options.fileDir || path.join(os.tmpdir(), 'edge-lambda'));
this.injectedHeadersFile = options.headersFile ? path.resolve(options.headersFile) : undefined;
this.path = this.serverless.service.custom.offlineEdgeLambda.path || '';
fs.mkdirpSync(this.cacheDir);
fs.mkdirpSync(this.fileDir);
this.origins = this.configureOrigins();
this.cacheService = new services_1.CacheService(this.cacheDir);
if (this.serverless.service.custom.offlineEdgeLambda.watchReload) {
this.watchFiles(path.join(this.path, '**/*'), Object.assign({ ignoreInitial: true, awaitWriteFinish: true, interval: 500, debounce: 750 }, options));
}
}
watchFiles(pattern, options) {
const watcher = chokidar.watch(pattern, options);
watcher.on('all', (0, debounce_1.debounce)((eventName, srcPath) => __awaiter(this, void 0, void 0, function* () {
console.log('Lambda files changed, syncing...');
yield this.extractBehaviors();
console.log('Lambda files synced');
}), options.debounce, true));
}
match(req) {
if (!req.url) {
return null;
}
const url = new url_1.URL(req.url, 'http://localhost');
for (const [, handler] of this.behaviors) {
if (handler.regex.test(url.pathname)) {
return handler;
}
}
return this.behaviors.get('*') || null;
}
start(port) {
return __awaiter(this, void 0, void 0, function* () {
this.started = true;
return new Promise((res, rej) => __awaiter(this, void 0, void 0, function* () {
yield this.listen(port);
// While the server is in a "restarting state" just restart the server
while (this.restarting) {
this.restarting = false;
yield this.listen(port, false);
}
res('Server shutting down ...');
}));
});
}
hasStarted() {
return this.started;
}
isRunning() {
return this.server !== null;
}
restart() {
return __awaiter(this, void 0, void 0, function* () {
if (this.restarting) {
return;
}
this.restarting = true;
this.purgeBehaviourFunctions();
yield this.shutdown();
});
}
shutdown() {
return __awaiter(this, void 0, void 0, function* () {
if (this.server !== null) {
yield this.server.close();
}
this.server = null;
});
}
listen(port, verbose = true) {
return __awaiter(this, void 0, void 0, function* () {
try {
yield this.extractBehaviors();
if (verbose) {
this.logStorage();
this.logBehaviors();
}
const app = (0, connect_1.default)();
app.use((0, middlewares_1.cloudfrontPost)());
app.use((0, body_parser_1.default)());
app.use((0, cookie_parser_1.default)());
app.use((0, middlewares_1.asyncMiddleware)((req, res) => __awaiter(this, void 0, void 0, function* () {
var _a;
if ((req.method || '').toUpperCase() === 'PURGE') {
yield this.purgeStorage();
res.statusCode = http_status_codes_1.StatusCodes.OK;
res.end();
return;
}
const handler = this.match(req);
if (!handler) {
res.statusCode = http_status_codes_1.StatusCodes.NOT_FOUND;
res.end();
return;
}
const customOrigin = handler.distribution in this.cfResources ?
(0, utils_1.getOriginFromCfDistribution)(handler.pattern, this.cfResources[handler.distribution]) :
null;
const cfEvent = (0, utils_1.convertToCloudFrontEvent)(req, this.builder('viewer-request'), this.injectedHeadersFile);
try {
const context = Object.assign(Object.assign({}, this.context), { functionName: handler.name });
const lifecycle = new services_1.CloudFrontLifecycle(this.serverless, this.options, cfEvent, context, this.cacheService, handler, customOrigin);
const response = yield lifecycle.run(req.url);
if (!response) {
throw new http_2.InternalServerError('No response set after full request lifecycle');
}
res.statusCode = parseInt(response.status, 10);
res.statusMessage = response.statusDescription || '';
const helper = new utils_1.CloudFrontHeadersHelper(response.headers);
for (const { key, value } of helper.asHttpHeaders()) {
if (value) {
res.setHeader(key, value);
}
}
if (response.bodyEncoding === 'base64') {
res.end(Buffer.from((_a = response.body) !== null && _a !== void 0 ? _a : '', 'base64'));
}
else {
res.end(response.body);
}
}
catch (err) {
this.handleError(err, res);
return;
}
})));
return new Promise(resolve => {
this.server = (0, http_1.createServer)(app);
this.server.listen(port);
this.server.on('close', (e) => {
resolve(e);
});
});
}
catch (err) {
console.error(err);
process.exit(1);
}
});
}
// Format errors
handleError(err, res) {
res.statusCode = err.statusCode || http_status_codes_1.StatusCodes.INTERNAL_SERVER_ERROR;
const payload = JSON.stringify(err.hasOwnProperty('getResponsePayload') ?
err.getResponsePayload() :
{
code: http_status_codes_1.StatusCodes.INTERNAL_SERVER_ERROR,
message: err.stack || err.message
});
res.end(payload);
}
purgeStorage() {
return __awaiter(this, void 0, void 0, function* () {
this.cacheService.purge();
});
}
configureOrigins() {
const { custom } = this.serverless.service;
const mappings = custom.offlineEdgeLambda.originMap || [];
return mappings.reduce((acc, item) => {
acc.set(item.pathPattern, new services_1.Origin(item.target));
return acc;
}, new Map());
}
extractBehaviors() {
var _a, e_1, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
const { functions } = this.serverless.service;
const behaviors = this.behaviors;
const lambdaDefs = Object.entries(functions)
.filter(([, fn]) => 'lambdaAtEdge' in fn);
behaviors.clear();
try {
for (var _d = true, lambdaDefs_1 = __asyncValues(lambdaDefs), lambdaDefs_1_1; lambdaDefs_1_1 = yield lambdaDefs_1.next(), _a = lambdaDefs_1_1.done, !_a;) {
_c = lambdaDefs_1_1.value;
_d = false;
try {
const [, def] = _c;
const pattern = def.lambdaAtEdge.pathPattern || '*';
const distribution = def.lambdaAtEdge.distribution || '';
if (!behaviors.has(pattern)) {
const origin = this.origins.get(pattern);
behaviors.set(pattern, new function_set_1.FunctionSet(pattern, distribution, this.log, origin, def.name));
}
const fnSet = behaviors.get(pattern);
// Don't try to register distributions that come from other sources
if (fnSet.distribution !== distribution) {
this.log(`Warning: pattern ${pattern} has registered handlers for cf distributions ${fnSet.distribution}` +
` and ${distribution}. There is no way to tell which distribution should be used so only ${fnSet.distribution}` +
` has been registered.`);
continue;
}
yield fnSet.setHandler(def.lambdaAtEdge.eventType, path.join(this.path, def.handler));
}
finally {
_d = true;
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_d && !_a && (_b = lambdaDefs_1.return)) yield _b.call(lambdaDefs_1);
}
finally { if (e_1) throw e_1.error; }
}
if (!behaviors.has('*')) {
behaviors.set('*', new function_set_1.FunctionSet('*', '', this.log, this.origins.get('*')));
}
});
}
purgeBehaviourFunctions() {
this.behaviors.forEach((behavior) => {
behavior.purgeLoadedFunctions();
});
}
logStorage() {
this.log(`Cache directory: file://${this.cacheDir}`);
this.log(`Files directory: file://${this.fileDir}`);
console.log();
}
logBehaviors() {
this.behaviors.forEach((behavior, key) => {
this.log(`Lambdas for path pattern ${key}` +
(behavior.distribution === '' ? ':' : ` on ${behavior.distribution}:`));
behavior.viewerRequest && this.log(`viewer-request => ${behavior.viewerRequest.path || ''}`);
behavior.originRequest && this.log(`origin-request => ${behavior.originRequest.path || ''}`);
behavior.originResponse && this.log(`origin-response => ${behavior.originResponse.path || ''}`);
behavior.viewerResponse && this.log(`viewer-response => ${behavior.viewerResponse.path || ''}`);
console.log(); // New line
});
}
}
exports.BehaviorRouter = BehaviorRouter;
//# sourceMappingURL=behavior-router.js.map