UNPKG

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
"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