From 77553aea767acd40a640fbf3c9fa154b88097dc0 Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Thu, 25 Jul 2019 14:39:39 -0300 Subject: [PATCH] feat: Spawn offline process with `sls offline` command (#214) sls offline builds Azure Functions files and spawns func host start in the same shell sls offline start spawns func host start in the same shell BaseService has spawn function that spawns and waits for child process in shell (routes stdout and stderr to internal logger) Using mock-spawn library to mock spawned child processes Add invocation commands to config constants --- README.md | 20 +++++-- package-lock.json | 64 +++++++------------- package.json | 1 + src/config.ts | 5 +- src/models/serverless.ts | 6 ++ src/plugins/func/azureFuncPlugin.test.ts | 18 +++++- src/plugins/offline/azureOfflinePlugin.ts | 13 +++- src/services/baseService.test.ts | 14 +++-- src/services/baseService.ts | 15 +++-- src/services/funcService.test.ts | 13 +++- src/services/offlineService.test.ts | 14 +++-- src/services/offlineService.ts | 73 +++++++++++++++++++++-- src/services/rollbackService.test.ts | 6 +- 13 files changed, 182 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index c6f018b8..7bb28e5a 100644 --- a/README.md +++ b/README.md @@ -34,17 +34,27 @@ $ npm i azure-functions-core-tools -g Then, at the root of your project directory, run: ```bash -# Builds necessary function bindings files +# Builds necessary function bindings files and starts the function app $ sls offline -# Starts the function app -$ npm start ``` The `offline` process will generate a directory for each of your functions, which will contain a file titled `function.json`. This will contain a relative reference to your handler file & exported function from that file as long as they are referenced correctly in `serverless.yml`. -The `npm start` script just runs `func host start`, but we included the `npm` script for ease of use. +After the necessary files are generated, it will start the function app from within the same shell. For HTTP functions, the local URLs will be displayed in the console when the function app is initialized. -To clean up files generated from the build, you can simply run: +To simply start the function app *without* building the files, run: + +```bash +$ sls offline start +``` + +To build the files *without* spawning the process to start the function app, run: + +```bash +$ sls offline build +``` + +To clean up files generated from the build, run: ```bash sls offline cleanup diff --git a/package-lock.json b/package-lock.json index 755ec013..ea294caa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1794,7 +1794,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -1967,7 +1966,6 @@ "version": "4.10.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "optional": true, "requires": { "bn.js": "^4.0.0", "inherits": "^2.0.1", @@ -2437,8 +2435,7 @@ "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "optional": true + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" }, "body-parser": { "version": "1.19.0", @@ -2543,8 +2540,7 @@ "brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "optional": true + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, "browser-process-hrtime": { "version": "0.1.3", @@ -2573,7 +2569,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "optional": true, "requires": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", @@ -2610,7 +2605,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "optional": true, "requires": { "bn.js": "^4.1.0", "randombytes": "^2.0.1" @@ -2707,8 +2701,7 @@ "buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "optional": true + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" }, "builtin-modules": { "version": "1.1.1", @@ -2895,7 +2888,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "optional": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -3273,7 +3265,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "optional": true, "requires": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -3286,7 +3277,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "optional": true, "requires": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -3750,7 +3740,6 @@ "version": "6.4.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", - "optional": true, "requires": { "bn.js": "^4.4.0", "brorand": "^1.0.1", @@ -3812,7 +3801,6 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", - "optional": true, "requires": { "prr": "~1.0.1" } @@ -4192,7 +4180,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "optional": true, "requires": { "d": "1", "es5-ext": "~0.10.14" @@ -4207,7 +4194,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "optional": true, "requires": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" @@ -5505,7 +5491,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "optional": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -5515,7 +5500,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "optional": true, "requires": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -5525,7 +5509,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "optional": true, "requires": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", @@ -5844,8 +5827,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "optional": true + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -7485,8 +7467,7 @@ "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "optional": true + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" }, "loose-envify": { "version": "1.4.0", @@ -7587,7 +7568,6 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "optional": true, "requires": { "hash-base": "^3.0.0", "inherits": "^2.0.1", @@ -7613,7 +7593,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", - "optional": true, "requires": { "errno": "^0.1.3", "readable-stream": "^2.0.1" @@ -7704,14 +7683,12 @@ "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "optional": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "optional": true + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "minimatch": { "version": "3.0.4", @@ -7766,6 +7743,15 @@ "integrity": "sha512-eBpLEjI6tK4RKK44BbUBQu89lrNh+5WeX3wf2U6Uwo6RtRGAQ77qvKeuuQh3lVXHF1aPndVww9VcjqmLThIdtA==", "dev": true }, + "mock-spawn": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/mock-spawn/-/mock-spawn-0.2.6.tgz", + "integrity": "sha1-s5wVocBnUEMQFEFR8sHeNE0Dk38=", + "dev": true, + "requires": { + "through": "2.3.x" + } + }, "moment": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", @@ -8309,7 +8295,6 @@ "version": "5.1.4", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", - "optional": true, "requires": { "asn1.js": "^4.0.0", "browserify-aes": "^1.0.0", @@ -8418,7 +8403,6 @@ "version": "3.0.17", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", - "optional": true, "requires": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", @@ -8621,8 +8605,7 @@ "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "optional": true + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" }, "pseudomap": { "version": "1.0.2", @@ -8689,7 +8672,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "optional": true, "requires": { "safe-buffer": "^5.1.0" } @@ -9102,7 +9084,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "optional": true, "requires": { "hash-base": "^3.0.0", "inherits": "^2.0.1" @@ -9492,7 +9473,6 @@ "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "optional": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -9688,8 +9668,7 @@ "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "optional": true + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" }, "source-map": { "version": "0.7.3", @@ -10246,8 +10225,7 @@ "tapable": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.9.tgz", - "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==", - "optional": true + "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==" }, "tar-stream": { "version": "1.6.2", @@ -11097,7 +11075,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", - "optional": true, "requires": { "source-list-map": "^2.0.0", "source-map": "~0.6.1" @@ -11106,8 +11083,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, diff --git a/package.json b/package.json index 5def538d..23dec2bb 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "jest": "^24.8.0", "jest-cli": "^24.8.0", "mock-fs": "^4.10.0", + "mock-spawn": "^0.2.6", "serverless": "^1.44.1", "shx": "^0.3.2", "typescript": "^3.4.5" diff --git a/src/config.ts b/src/config.ts index 9cf10474..36414eef 100644 --- a/src/config.ts +++ b/src/config.ts @@ -9,6 +9,9 @@ export const configConstants = { functionAppDomain: ".azurewebsites.net", functionsAdminApiPath: "/admin/functions/", functionsApiPath: "/api/functions", + funcCoreTools: "func", + funcCoreToolsArgs: ["host", "start"], + funcConsoleColor: "blue", jsonContentType: "application/json", logInvocationsApiPath: "/azurejobs/api/functions/definitions/", logOutputApiPath: "/azurejobs/api/log/output/", @@ -21,4 +24,4 @@ export const configConstants = { scmZipDeployApiPath: "/api/zipdeploy" }; -export default configConstants; \ No newline at end of file +export default configConstants; diff --git a/src/models/serverless.ts b/src/models/serverless.ts index c15adf1b..240b6ca0 100644 --- a/src/models/serverless.ts +++ b/src/models/serverless.ts @@ -94,3 +94,9 @@ export interface ServerlessCommandMap { export interface ServerlessAzureOptions extends Serverless.Options { resourceGroup?: string; } + +export interface ServerlessLogOptions { + underline?: boolean; + bold?: boolean; + color?: string; +} diff --git a/src/plugins/func/azureFuncPlugin.test.ts b/src/plugins/func/azureFuncPlugin.test.ts index 57db0e87..9869dbd2 100644 --- a/src/plugins/func/azureFuncPlugin.test.ts +++ b/src/plugins/func/azureFuncPlugin.test.ts @@ -44,7 +44,11 @@ describe("Azure Func Plugin", () => { const plugin = new AzureFuncPlugin(sls, options); await invokeHook(plugin, "func:add:add"); - expect(sls.cli.log).toBeCalledWith("Need to provide a name of function to add") + expect(sls.cli.log).toBeCalledWith( + "Need to provide a name of function to add", + undefined, + undefined + ) }); it("returns with pre-existing function", async () => { @@ -98,7 +102,11 @@ describe("Azure Func Plugin", () => { const options = MockFactory.createTestServerlessOptions(); const plugin = new AzureFuncPlugin(sls, options); await invokeHook(plugin, "func:remove:remove"); - expect(sls.cli.log).toBeCalledWith("Need to provide a name of function to remove") + expect(sls.cli.log).toBeCalledWith( + "Need to provide a name of function to remove", + undefined, + undefined + ) }); it("returns with non-existing function", async () => { @@ -107,7 +115,11 @@ describe("Azure Func Plugin", () => { options["name"] = "myNonExistingFunction"; const plugin = new AzureFuncPlugin(sls, options); await invokeHook(plugin, "func:remove:remove"); - expect(sls.cli.log).toBeCalledWith("Function myNonExistingFunction does not exist"); + expect(sls.cli.log).toBeCalledWith( + "Function myNonExistingFunction does not exist", + undefined, + undefined + ); }); it("deletes directory and updates serverless.yml", async () => { diff --git a/src/plugins/offline/azureOfflinePlugin.ts b/src/plugins/offline/azureOfflinePlugin.ts index 2f5f8797..9fd7cb0a 100644 --- a/src/plugins/offline/azureOfflinePlugin.ts +++ b/src/plugins/offline/azureOfflinePlugin.ts @@ -15,6 +15,7 @@ export class AzureOfflinePlugin extends AzureBasePlugin { "before:offline:offline": this.azureOfflineBuild.bind(this), "offline:build:build": this.azureOfflineBuild.bind(this), "offline:offline": this.azureOfflineStart.bind(this), + "offline:start:start": this.azureOfflineStart.bind(this), "offline:cleanup:cleanup": this.azureOfflineCleanup.bind(this), }; @@ -25,6 +26,12 @@ export class AzureOfflinePlugin extends AzureBasePlugin { "offline", ], commands: { + start: { + usage: "Start Azure Function app - assumes offline build has already occurred", + lifecycleEvents: [ + "start" + ] + }, build: { usage: "Build necessary files for running Azure Function App offline", lifecycleEvents: [ @@ -43,14 +50,14 @@ export class AzureOfflinePlugin extends AzureBasePlugin { } private async azureOfflineBuild(){ - this.offlineService.build(); + await this.offlineService.build(); } private async azureOfflineStart(){ - this.offlineService.start(); + await this.offlineService.start(); } private async azureOfflineCleanup(){ - this.offlineService.cleanup(); + await this.offlineService.cleanup(); } } diff --git a/src/services/baseService.test.ts b/src/services/baseService.test.ts index 1ecf667d..d001cb8e 100644 --- a/src/services/baseService.test.ts +++ b/src/services/baseService.test.ts @@ -1,14 +1,16 @@ +import fs from "fs"; +import mockFs from "mock-fs"; import Serverless from "serverless"; +import { ServerlessAzureOptions } from "../models/serverless"; +import { Utils } from "../shared/utils"; +import { MockFactory } from "../test/mockFactory"; +import { BaseService } from "./baseService"; + jest.mock("axios", () => jest.fn()); import axios from "axios"; -import mockFs from "mock-fs"; -import { MockFactory } from "../test/mockFactory"; + jest.mock("request", () => MockFactory.createTestMockRequestFactory()); import request from "request"; -import fs from "fs"; -import { BaseService } from "./baseService"; -import { ServerlessAzureOptions } from "../models/serverless"; -import { Utils } from "../shared/utils"; class MockService extends BaseService { public constructor(serverless: Serverless, options?: ServerlessAzureOptions) { diff --git a/src/services/baseService.ts b/src/services/baseService.ts index 7b21371b..c5715f45 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -1,13 +1,18 @@ +import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; import axios from "axios"; import fs from "fs"; import request from "request"; import Serverless from "serverless"; -import { ServerlessAzureOptions, ServerlessAzureFunctionConfig } from "../models/serverless"; import { StorageAccountResource } from "../armTemplates/resources/storageAccount"; import { configConstants } from "../config"; -import { DeploymentConfig, ServerlessAzureConfig } from "../models/serverless"; +import { + DeploymentConfig, + ServerlessAzureConfig, + ServerlessAzureFunctionConfig, + ServerlessAzureOptions, + ServerlessLogOptions +} from "../models/serverless"; import { Guard } from "../shared/guard"; -import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; import { Utils } from "../shared/utils"; export abstract class BaseService { @@ -174,8 +179,8 @@ export abstract class BaseService { * Log message to Serverless CLI * @param message Message to log */ - protected log(message: string) { - this.serverless.cli.log(message); + protected log(message: string, options?: ServerlessLogOptions, entity?: string,) { + (this.serverless.cli.log as any)(message, entity, options); } /** diff --git a/src/services/funcService.test.ts b/src/services/funcService.test.ts index b42bb755..fd0d9d1c 100644 --- a/src/services/funcService.test.ts +++ b/src/services/funcService.test.ts @@ -41,7 +41,7 @@ describe("Azure Func Service", () => { const service = createService(sls, options); await service.add(); - expect(sls.cli.log).toBeCalledWith("Need to provide a name of function to add") + expect(sls.cli.log).toBeCalledWith("Need to provide a name of function to add", undefined, undefined); }); it("returns with pre-existing function", async () => { @@ -94,7 +94,11 @@ describe("Azure Func Service", () => { const options = MockFactory.createTestServerlessOptions(); const service = createService(sls, options); await service.remove(); - expect(sls.cli.log).toBeCalledWith("Need to provide a name of function to remove") + expect(sls.cli.log).toBeCalledWith( + "Need to provide a name of function to remove", + undefined, + undefined + ) }); it("returns with non-existing function", async () => { @@ -103,7 +107,10 @@ describe("Azure Func Service", () => { options["name"] = "myNonExistingFunction"; const service = createService(sls, options); await service.remove(); - expect(sls.cli.log).toBeCalledWith("Function myNonExistingFunction does not exist"); + expect(sls.cli.log).toBeCalledWith("Function myNonExistingFunction does not exist", + undefined, + undefined + ); }); it("deletes directory and updates serverless.yml", async () => { diff --git a/src/services/offlineService.test.ts b/src/services/offlineService.test.ts index 979dc0c3..c8fc4f49 100644 --- a/src/services/offlineService.test.ts +++ b/src/services/offlineService.test.ts @@ -1,5 +1,6 @@ import fs from "fs"; import mockFs from "mock-fs"; +import mockSpawn from "mock-spawn"; import path from "path"; import Serverless from "serverless"; import { MockFactory } from "../test/mockFactory"; @@ -7,6 +8,10 @@ import { OfflineService } from "./offlineService"; describe("Offline Service", () => { + const mySpawn = mockSpawn(); + require("child_process").spawn = mySpawn; + mySpawn.setDefault(mySpawn.simple(0, "Exit code")); + function createService(sls?: Serverless): OfflineService { return new OfflineService( sls || MockFactory.createTestServerless(), @@ -112,9 +117,10 @@ describe("Offline Service", () => { const sls = MockFactory.createTestServerless(); const service = createService(sls); await service.start(); - // Trivial test for now. In the future, this process - // may spawn the start process itself rather than telling - // the user how to do it. - expect(sls.cli.log).toBeCalledTimes(3); + const calls = mySpawn.calls; + expect(calls).toHaveLength(1); + const call = calls[0]; + expect(call.command).toEqual("func"); + expect(call.args).toEqual(["host", "start"]); }); }); diff --git a/src/services/offlineService.ts b/src/services/offlineService.ts index 35f62e37..40ac8551 100644 --- a/src/services/offlineService.ts +++ b/src/services/offlineService.ts @@ -1,5 +1,7 @@ -import Serverless from "serverless"; +import { spawn } from "child_process"; import fs from "fs"; +import Serverless from "serverless"; +import configConstants from "../config"; import { BaseService } from "./baseService"; import { PackageService } from "./packageService"; @@ -50,9 +52,70 @@ export class OfflineService extends BaseService { this.log("Finished cleaning up offline files"); } - public start() { - this.log("Run 'npm start' or 'func host start' to run service locally"); - this.log("Make sure you have Azure Functions Core Tools installed"); - this.log("If not installed run 'npm i azure-functions-core-tools -g") + /** + * Spawn `func host start` from core func tools + */ + public async start() { + await this.spawn(configConstants.funcCoreTools, configConstants.funcCoreToolsArgs); + } + + /** + * Spawn a Node child process with predefined environment variables + * @param command CLI Command - NO ARGS + * @param args Array of arguments for CLI command + * @param env Additional environment variables to be set in addition to + * predefined variables in `serverless.yml` + */ + private spawn(command: string, args?: string[], env?: any): Promise { + env = { + ...this.serverless.service.provider["environment"], + ...env + } + this.log(`Spawning process '${command} ${args.join(" ")}'`); + return new Promise((resolve, reject) => { + const childProcess = spawn(command, args, {env}); + + childProcess.stdout.on("data", (data) => { + this.log(data, { + color: configConstants.funcConsoleColor, + }, command); + }); + + childProcess.stderr.on("data", (data) => { + this.log(data, { + color: "red", + }, command); + }) + + childProcess.on("message", (message) => { + this.log(message, { + color: configConstants.funcConsoleColor, + }, command); + }); + + childProcess.on("error", (err) => { + this.log(`${err}`, { + color: "red" + }, command); + reject(err); + }); + + childProcess.on("exit", (code) => { + this.log(`Exited with code: ${code}`, { + color: (code === 0) ? "green" : "red", + }, command); + }); + + childProcess.on("close", (code) => { + this.log(`Closed with code: ${code}`, { + color: (code === 0) ? "green" : "red", + }, command); + resolve(); + }); + + childProcess.on("disconnect", () => { + this.log("Process disconnected"); + }); + }); } } diff --git a/src/services/rollbackService.test.ts b/src/services/rollbackService.test.ts index 547bbe28..ffdf0e9a 100644 --- a/src/services/rollbackService.test.ts +++ b/src/services/rollbackService.test.ts @@ -74,7 +74,11 @@ describe("Rollback Service", () => { const options = {} as any; const service = createService(sls, options); await service.rollback(); - expect(sls.cli.log).lastCalledWith(deploymentString); + expect(sls.cli.log).lastCalledWith( + deploymentString, + undefined, + undefined + ); }); it("should return early with invalid timestamp", async () => {