diff --git a/README.md b/README.md index fd367a8..11d2c64 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ _serverless.yml_ ```yaml service: name: edge-lambdas - + plugins: - serverless-offline-edge-lambda @@ -60,7 +60,7 @@ a configuration option `path` that it uses to resolve function handlers. ```yaml plugins: - serverless-plugin-typescript - + custom: offlineEdgeLambda: path: '.build' @@ -70,8 +70,40 @@ For usage with `serverless-webpack` and `serverless-bundle` the config is simila ```yaml plugins: - serverless-webpack # or serverless-bundle - + custom: offlineEdgeLambda: path: './.webpack/service/' ``` + +### Hot Reload Support + +Hot reload for serverless-esbuild and serverless-plugin-typescript are available with extra configuration. + +The watch/reload mechanism is available form serverless-webpack, but is disabled by default for esbuild and typescript. + +The flag "watchReload: true" will turn on the watcher so that typescript and esbuild solutions use the watcher to hot reload the handlers. +The path to the built handlers must be specified for the watcher to work correctly. + +example: +```yaml +custom: + offlineEdgeLambda: + path: '.esbuild/service' + watchReload: true +``` + +Additional options can be used to modify the behavior of the file watcher and debounce logic (ignoreInitial, awaitWriteFinish, interval, debounce, and any other chokidar option). + +example: + +```yaml +custom: + offlineEdgeLambda: + path: '.dist/service' + watchReload: true + ignoreInitial: true + awaitWriteFinish: true + interval: 500, + debounce: 750 +``` diff --git a/package-lock.json b/package-lock.json index d4063e1..b52abc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "license": "Apache-2.0", "dependencies": { "body-parser": "^1.20.0", + "chokidar": "^3.5.3", "connect": "^3.7.0", "cookie-parser": "^1.4.6", "flat-cache": "^3.0.4", @@ -3253,7 +3254,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "peer": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -3560,7 +3560,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "peer": true, "engines": { "node": ">=8" } @@ -4109,10 +4108,15 @@ } }, "node_modules/chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", - "peer": true, + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -7360,7 +7364,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "peer": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -8620,7 +8623,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11412,6 +11414,11 @@ "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, "engines": { "node": ">=0.10.0" } @@ -12640,7 +12647,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "peer": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -18112,7 +18118,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "peer": true, "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -18377,8 +18382,7 @@ "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "peer": true + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, "bindings": { "version": "1.5.0", @@ -18818,10 +18822,9 @@ } }, "chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", - "peer": true, + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "requires": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -21438,7 +21441,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "peer": true, "requires": { "binary-extensions": "^2.0.0" } @@ -22477,8 +22479,7 @@ "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "peer": true + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, "normalize-url": { "version": "6.1.0", @@ -25292,7 +25293,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "peer": true, "requires": { "picomatch": "^2.2.1" } diff --git a/package.json b/package.json index 00c0421..5d7a745 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "release": "semantic-release --no-ci", "release:dry-run": "semantic-release --no-ci --dry-run", "snyk-protect": "snyk protect", - "prepare": "npm run snyk-protect" + "prepare": "npm run snyk-protect && npm run build" }, "config": { "commitizen": { @@ -49,6 +49,7 @@ ], "dependencies": { "body-parser": "^1.20.0", + "chokidar": "^3.5.3", "connect": "^3.7.0", "cookie-parser": "^1.4.6", "flat-cache": "^3.0.4", diff --git a/src/behavior-router.ts b/src/behavior-router.ts index bbbe0be..f5bfd20 100644 --- a/src/behavior-router.ts +++ b/src/behavior-router.ts @@ -2,13 +2,14 @@ import { Context } from 'aws-lambda'; import bodyParser from 'body-parser'; import connect, { HandleFunction } from 'connect'; import cookieParser from 'cookie-parser'; +import * as chokidar from 'chokidar'; import * as fs from 'fs-extra'; import { createServer, Server, IncomingMessage, ServerResponse } from 'http'; import { StatusCodes } from 'http-status-codes'; import * as os from 'os'; import * as path from 'path'; import { URL } from 'url'; - +import { debounce } from './utils/debounce'; import { HttpError, InternalServerError } from './errors/http'; import { FunctionSet } from './function-set'; import { asyncMiddleware, cloudfrontPost } from './middlewares'; @@ -62,6 +63,25 @@ export class BehaviorRouter { this.origins = this.configureOrigins(); this.cacheService = new CacheService(this.cacheDir); + + if (this.serverless.service.custom.offlineEdgeLambda.watchReload) { + this.watchFiles(path.join(this.path, '**/*'), { + ignoreInitial: true, + awaitWriteFinish: true, + interval: 500, + debounce: 750, + ...options, + }); + } + } + + watchFiles(pattern: any, options: any) { + const watcher = chokidar.watch(pattern, options); + watcher.on('all', debounce(async (eventName, srcPath) => { + console.log('Lambda files changed, syncing...'); + await this.extractBehaviors(); + console.log('Lambda files synced'); + }, options.debounce, true)); } match(req: IncomingMessage): FunctionSet | null { diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts new file mode 100644 index 0000000..a7f813f --- /dev/null +++ b/src/utils/debounce.ts @@ -0,0 +1,22 @@ +export const debounce = + function (func: { (eventName: any, srcPath: any): Promise; apply?: any; }, threshold: number, execAsap: boolean) { + let timeout: any; + return function debounced(this: any) { + const obj = this; + const args = arguments; + + function delayed() { + if (!execAsap) { + func.apply(obj, args); + } + timeout = undefined; + } + + if (timeout) { + clearTimeout(timeout); + } else if (execAsap) { + func.apply(obj, args); + } + timeout = setTimeout(delayed, threshold || 100); + }; +}; diff --git a/src/utils/load-module.ts b/src/utils/load-module.ts index 9ba6575..8757bf2 100644 --- a/src/utils/load-module.ts +++ b/src/utils/load-module.ts @@ -1,5 +1,5 @@ import { resolve } from 'path'; -import {clearModule} from './clear-module'; +import { clearModule } from './clear-module'; export class ModuleLoader { protected loadedModules: string[] = []; @@ -15,6 +15,7 @@ export class ModuleLoader { const [, modulePath, functionName] = match; const absPath = resolve(modulePath); + delete require.cache[require.resolve(absPath)]; const module = await import(absPath); this.loadedModules.push(absPath);