From 39c3621a0b2c9b77f818c7415a1d2549364e9aa5 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Mon, 1 Jul 2019 16:24:11 -0700 Subject: [PATCH 1/5] Unify resolution logic --- package-lock.json | 127 +++++++++++++++++------ src/emulator/functionsEmulatorRuntime.ts | 68 +++++++----- 2 files changed, 133 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index 95c3eb4cd37..21b9974f920 100644 --- a/package-lock.json +++ b/package-lock.json @@ -230,6 +230,7 @@ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz", "integrity": "sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw==", "dev": true, + "optional": true, "requires": { "abort-controller": "^3.0.0", "extend": "^3.0.2", @@ -319,7 +320,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true + "dev": true, + "optional": true }, "retry-request": { "version": "4.0.0", @@ -399,7 +401,8 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.4.0.tgz", "integrity": "sha512-4yAHDC52TEMCNcMzVC8WlqnKKKq+Ssi2lXoUg9zWWkZ6U6tq9ZBRYLHHCRdfU+EU9YJsVmivwGcKYCjRGjnf4Q==", - "dev": true + "dev": true, + "optional": true }, "@google-cloud/storage": { "version": "2.5.0", @@ -515,31 +518,36 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", - "dev": true + "dev": true, + "optional": true }, "@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "dev": true + "dev": true, + "optional": true }, "@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "dev": true + "dev": true, + "optional": true }, "@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", - "dev": true + "dev": true, + "optional": true }, "@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", "dev": true, + "optional": true, "requires": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -549,31 +557,36 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", - "dev": true + "dev": true, + "optional": true }, "@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", - "dev": true + "dev": true, + "optional": true }, "@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", - "dev": true + "dev": true, + "optional": true }, "@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", - "dev": true + "dev": true, + "optional": true }, "@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", - "dev": true + "dev": true, + "optional": true }, "@sinonjs/commons": { "version": "1.4.0", @@ -774,7 +787,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==", - "dev": true + "dev": true, + "optional": true }, "@types/mime": { "version": "2.0.1", @@ -940,6 +954,7 @@ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "dev": true, + "optional": true, "requires": { "event-target-shim": "^5.0.0" } @@ -970,6 +985,7 @@ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", "dev": true, + "optional": true, "requires": { "es6-promisify": "^5.0.0" } @@ -1263,7 +1279,8 @@ "version": "7.2.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==", - "dev": true + "dev": true, + "optional": true }, "binary-extensions": { "version": "1.13.1", @@ -2253,6 +2270,7 @@ "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", "dev": true, + "optional": true, "requires": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", @@ -2347,13 +2365,15 @@ "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", - "dev": true + "dev": true, + "optional": true }, "es6-promisify": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", "dev": true, + "optional": true, "requires": { "es6-promise": "^4.0.3" } @@ -2573,7 +2593,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true + "dev": true, + "optional": true }, "execa": { "version": "0.7.0", @@ -2809,7 +2830,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz", "integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ==", - "dev": true + "dev": true, + "optional": true }, "fast-url-parser": { "version": "1.1.3", @@ -3193,7 +3215,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -3211,11 +3234,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3228,15 +3253,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3339,7 +3367,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3349,6 +3378,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3361,17 +3391,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3388,6 +3421,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3460,7 +3494,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3470,6 +3505,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3545,7 +3581,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3575,6 +3612,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3592,6 +3630,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3630,11 +3669,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -3649,6 +3690,7 @@ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.0.1.tgz", "integrity": "sha512-c1NXovTxkgRJTIgB2FrFmOFg4YIV6N/bAa4f/FZ4jIw13Ql9ya/82x69CswvotJhbV3DiGnlTZwoq2NVXk2Irg==", "dev": true, + "optional": true, "requires": { "abort-controller": "^3.0.0", "extend": "^3.0.2", @@ -3710,6 +3752,7 @@ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz", "integrity": "sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw==", "dev": true, + "optional": true, "requires": { "abort-controller": "^3.0.0", "extend": "^3.0.2", @@ -3722,6 +3765,7 @@ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "dev": true, + "optional": true, "requires": { "event-target-shim": "^5.0.0" } @@ -3810,7 +3854,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true + "dev": true, + "optional": true }, "write-file-atomic": { "version": "2.4.3", @@ -4071,7 +4116,8 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.1.1.tgz", "integrity": "sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==", - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", @@ -4275,6 +4321,7 @@ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", "dev": true, + "optional": true, "requires": { "agent-base": "^4.1.0", "debug": "^3.1.0" @@ -4285,6 +4332,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "dev": true, + "optional": true, "requires": { "ms": "^2.1.1" } @@ -4293,7 +4341,8 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "dev": true, + "optional": true } } }, @@ -4799,6 +4848,7 @@ "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz", "integrity": "sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4=", "dev": true, + "optional": true, "requires": { "bignumber.js": "^7.0.0" } @@ -5141,7 +5191,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", - "dev": true + "dev": true, + "optional": true }, "lowercase-keys": { "version": "1.0.1", @@ -5609,7 +5660,8 @@ "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", - "dev": true + "dev": true, + "optional": true }, "node-forge": { "version": "0.7.6", @@ -6200,6 +6252,7 @@ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", "dev": true, + "optional": true, "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -6240,6 +6293,7 @@ "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", "dev": true, + "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -6250,6 +6304,7 @@ "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", "dev": true, + "optional": true, "requires": { "duplexify": "^3.6.0", "inherits": "^2.0.3", @@ -6967,6 +7022,7 @@ "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", "dev": true, + "optional": true, "requires": { "stubs": "^3.0.0" } @@ -6975,7 +7031,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", - "dev": true + "dev": true, + "optional": true }, "streamsearch": { "version": "0.1.2", @@ -7068,7 +7125,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", - "dev": true + "dev": true, + "optional": true }, "superagent": { "version": "3.8.3", @@ -7287,6 +7345,7 @@ "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz", "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==", "dev": true, + "optional": true, "requires": { "https-proxy-agent": "^2.2.1", "node-fetch": "^2.2.0", diff --git a/src/emulator/functionsEmulatorRuntime.ts b/src/emulator/functionsEmulatorRuntime.ts index f7ab1fd37a1..c21ee748dcc 100644 --- a/src/emulator/functionsEmulatorRuntime.ts +++ b/src/emulator/functionsEmulatorRuntime.ts @@ -78,6 +78,7 @@ interface ModuleResolution { declared: boolean; installed: boolean; version?: string; + resolution?: string; } interface ModuleVersion { @@ -184,9 +185,15 @@ class Proxied { async function resolveDeveloperNodeModule( frb: FunctionsRuntimeBundle, - pkg: PackageJSON, name: string ): Promise { + // TODO: Can we cache this call? Should we? + const pkg = requirePackageJson(frb); + if (!pkg) { + new EmulatorLog("SYSTEM", "missing-package-json", "").log(); + throw "Could not find package.json"; + } + const dependencies = pkg.dependencies; const devDependencies = pkg.devDependencies; const isInPackageJSON = dependencies[name] || devDependencies[name]; @@ -197,35 +204,32 @@ async function resolveDeveloperNodeModule( } // Once we know it's in the package.json, make sure it's actually `npm install`ed - const modResolution = await requireResolveAsync(name, { paths: [frb.cwd] }).catch(NoOp); - - if (!modResolution) { + const resolveResult = await requireResolveAsync(name, { paths: [frb.cwd] }).catch(NoOp); + if (!resolveResult) { return { declared: true, installed: false }; } - const modPackageJSON = require(path.join(findModuleRoot(name, modResolution), "package.json")); + const modPackageJSON = require(path.join(findModuleRoot(name, resolveResult), "package.json")); - return { + const moduleResolution: ModuleResolution = { declared: true, installed: true, version: modPackageJSON.version, + resolution: resolveResult, }; + + new EmulatorLog("DEBUG", "runtime-status", `Resolved module ${name}: ${JSON.stringify(moduleResolution)}`).log(); + return moduleResolution; } async function verifyDeveloperNodeModules(frb: FunctionsRuntimeBundle): Promise { - const pkg = requirePackageJson(frb); - if (!pkg) { - new EmulatorLog("SYSTEM", "missing-package-json", "").log(); - return false; - } - const modBundles = [ { name: "firebase-admin", isDev: false, minVersion: 8 }, { name: "firebase-functions", isDev: false, minVersion: 3 }, ]; for (const modBundle of modBundles) { - const resolution = await resolveDeveloperNodeModule(frb, pkg, modBundle.name); + const resolution = await resolveDeveloperNodeModule(frb, modBundle.name); /* If there's no reference to the module in their package.json, prompt them to install it @@ -408,11 +412,13 @@ function InitializeNetworkFiltering(frb: FunctionsRuntimeBundle): void { The relevant firebase-functions code is: https://github.com/firebase/firebase-functions/blob/9e3bda13565454543b4c7b2fd10fb627a6a3ab97/src/providers/https.ts#L66 */ -async function InitializeFirebaseFunctionsStubs(functionsDir: string): Promise { - const firebaseFunctionsResolution = await requireResolveAsync("firebase-functions", { - paths: [functionsDir], - }); - const firebaseFunctionsRoot = findModuleRoot("firebase-functions", firebaseFunctionsResolution); +async function InitializeFirebaseFunctionsStubs(frb: FunctionsRuntimeBundle): Promise { + const firebaseFunctionsResolution = await resolveDeveloperNodeModule(frb, "firebase-functions"); + if (!firebaseFunctionsResolution.resolution) { + throw "Could not resolve 'firebase-functions'" + } + + const firebaseFunctionsRoot = findModuleRoot("firebase-functions", firebaseFunctionsResolution.resolution); const httpsProviderResolution = path.join(firebaseFunctionsRoot, "lib/providers/https"); const httpsProvider = require(httpsProviderResolution); @@ -477,10 +483,15 @@ async function getGRPCInsecureCredential(frb: FunctionsRuntimeBundle): Promise { - const adminResolution = await requireResolveAsync("firebase-admin", { paths: [frb.cwd] }); - const localAdminModule = require(adminResolution); + const adminResolution = await resolveDeveloperNodeModule(frb, "firebase-admin"); + if (!adminResolution.resolution) { + throw "Could not resolve 'firebase-admin'"; + } + + const localAdminModule = require(adminResolution.resolution); + + let hasInitializedFirestore = false; - let hasInitializedSettings = false; /* If we can't get sslCreds that means either grpc or grpc-js doesn't exist. If this is the save, then there's probably something really wrong (like a failed node-gyp build). If that's the case @@ -491,14 +502,14 @@ async function InitializeFirebaseAdminStubs(frb: FunctionsRuntimeBundle): Promis const isEnabled = isFeatureEnabled(frb, "admin_stubs"); if (!isEnabled) { - if (!hasInitializedSettings) { + if (!hasInitializedFirestore) { app.firestore().settings(userSettings); - hasInitializedSettings = true; + hasInitializedFirestore = true; } return; } - if (!hasInitializedSettings && frb.ports.firestore) { + if (!hasInitializedFirestore && frb.ports.firestore) { app.firestore().settings({ projectId: frb.projectId, port: frb.ports.firestore, @@ -519,7 +530,7 @@ async function InitializeFirebaseAdminStubs(frb: FunctionsRuntimeBundle): Promis ).log(); } - hasInitializedSettings = true; + hasInitializedFirestore = true; }; adminModuleProxy = new Proxied(localAdminModule) @@ -532,7 +543,6 @@ async function InitializeFirebaseAdminStubs(frb: FunctionsRuntimeBundle): Promis const config = JSON.parse(process.env.FIREBASE_CONFIG || "{}"); new EmulatorLog("SYSTEM", "default-admin-app-used", `config=${config}`).log(); - // TODO: Is there any possible harm in this? config.credential = makeFakeCredentials(); if (frb.ports.database) { @@ -566,13 +576,15 @@ async function InitializeFirebaseAdminStubs(frb: FunctionsRuntimeBundle): Promis }) .finalize(); - require.cache[adminResolution] = { + // Stub the admin module in the require cache + require.cache[adminResolution.resolution] = { exports: adminModuleProxy, }; new EmulatorLog("DEBUG", "runtime-status", "firebase-admin has been stubbed.", { adminResolution, }).log(); + return adminModuleProxy; } @@ -862,7 +874,7 @@ async function main(): Promise { await InitializeFunctionsConfigHelper(frb.cwd); } - await InitializeFirebaseFunctionsStubs(frb.cwd); + await InitializeFirebaseFunctionsStubs(frb); await InitializeFirebaseAdminStubs(frb); let triggers: EmulatedTriggerMap; From 425576c069731a29f62bb28e0d6c9af6dffab340 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Tue, 2 Jul 2019 10:42:21 -0700 Subject: [PATCH 2/5] Fix issues, tests pass --- src/emulator/functionsEmulator.ts | 6 +- src/emulator/functionsEmulatorRuntime.ts | 246 ++++++++++++----------- 2 files changed, 134 insertions(+), 118 deletions(-) diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index 1520e70a028..d8d7042b672 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -387,7 +387,11 @@ You can probably fix this by running "npm install ${ EmulatorLogger.log("USER", `${clc.blackBright("> ")} ${log.text}`); break; case "DEBUG": - EmulatorLogger.log("DEBUG", log.text); + if (log.data && log.data !== {}) { + EmulatorLogger.log("DEBUG", `${log.text} data=${JSON.stringify(log.data)}`); + } else { + EmulatorLogger.log("DEBUG", log.text); + } break; case "INFO": EmulatorLogger.logLabeled("BULLET", "functions", log.text); diff --git a/src/emulator/functionsEmulatorRuntime.ts b/src/emulator/functionsEmulatorRuntime.ts index c21ee748dcc..545926f2752 100644 --- a/src/emulator/functionsEmulatorRuntime.ts +++ b/src/emulator/functionsEmulatorRuntime.ts @@ -16,9 +16,11 @@ import * as admin from "firebase-admin"; import * as bodyParser from "body-parser"; import { URL } from "url"; import * as _ from "lodash"; +import { Message } from "firebase-functions/lib/providers/pubsub"; -let app: admin.app.App; +let defaultApp: admin.app.App; let adminModuleProxy: typeof admin; +let hasInitializedFirestore = false; function isFeatureEnabled( frb: FunctionsRuntimeBundle, @@ -138,26 +140,26 @@ class Proxied { }); } + /** + * Calling .when("a", () => "b") will rewrite obj["a"] to be equal to "b" + */ when(key: string, value: (target: any, key: string) => any): Proxied { - /* - Calling .when("a", () => "b") will rewrite obj["a"] to be equal to "b" - */ this.rewrites[key] = value; return this as Proxied; } + /** + * Calling .any(() => "b") will rewrite all fields on obj to be equal to "b" + */ any(value: (target: any, key: string) => any): Proxied { - /* - Calling .any(() => "b") will rewrite all fields on obj to be equal to "b" - */ this.anyValue = value; return this as Proxied; } + /** + * Calling .applied(() => "b") will make obj() equal to "b" + */ applied(value: () => any): Proxied { - /* - Calling .applied(() => "b") will make obj() equal to "b" - */ this.appliedValue = value; return this as Proxied; } @@ -191,7 +193,7 @@ async function resolveDeveloperNodeModule( const pkg = requirePackageJson(frb); if (!pkg) { new EmulatorLog("SYSTEM", "missing-package-json", "").log(); - throw "Could not find package.json"; + throw new Error("Could not find package.json"); } const dependencies = pkg.dependencies; @@ -218,7 +220,7 @@ async function resolveDeveloperNodeModule( resolution: resolveResult, }; - new EmulatorLog("DEBUG", "runtime-status", `Resolved module ${name}: ${JSON.stringify(moduleResolution)}`).log(); + logDebug(`Resolved module ${name}`, moduleResolution); return moduleResolution; } @@ -324,13 +326,11 @@ function InitializeNetworkFiltering(frb: FunctionsRuntimeBundle): void { }; networkingModules.push(gaxModule); - new EmulatorLog("DEBUG", "runtime-status", `Found google-gax at ${gaxPath}`).log(); + logDebug(`Found google-gax at ${gaxPath}`); } catch (err) { - new EmulatorLog( - "DEBUG", - "runtime-status", + logDebug( `Couldn't find @google-cloud/firestore or google-gax, this may be okay if using @google-cloud/firestore@2.0.0` - ).log(); + ); } const history: { [href: string]: boolean } = {}; @@ -398,7 +398,7 @@ function InitializeNetworkFiltering(frb: FunctionsRuntimeBundle): void { return { name: bundle.name, status: "mocked" }; }); - new EmulatorLog("DEBUG", "runtime-status", "Outgoing network have been stubbed.", results).log(); + logDebug("Outgoing network have been stubbed.", results); } /* This stub handles a very specific use-case, when a developer (incorrectly) provides a HTTPS handler @@ -415,10 +415,13 @@ https://github.com/firebase/firebase-functions/blob/9e3bda13565454543b4c7b2fd10f async function InitializeFirebaseFunctionsStubs(frb: FunctionsRuntimeBundle): Promise { const firebaseFunctionsResolution = await resolveDeveloperNodeModule(frb, "firebase-functions"); if (!firebaseFunctionsResolution.resolution) { - throw "Could not resolve 'firebase-functions'" + throw new Error("Could not resolve 'firebase-functions'"); } - const firebaseFunctionsRoot = findModuleRoot("firebase-functions", firebaseFunctionsResolution.resolution); + const firebaseFunctionsRoot = findModuleRoot( + "firebase-functions", + firebaseFunctionsResolution.resolution + ); const httpsProviderResolution = path.join(firebaseFunctionsRoot, "lib/providers/https"); const httpsProvider = require(httpsProviderResolution); @@ -485,32 +488,83 @@ async function getGRPCInsecureCredential(frb: FunctionsRuntimeBundle): Promise { const adminResolution = await resolveDeveloperNodeModule(frb, "firebase-admin"); if (!adminResolution.resolution) { - throw "Could not resolve 'firebase-admin'"; + throw new Error("Could not resolve 'firebase-admin'"); } + // If we can't get sslCreds that means either grpc or grpc-js doesn't exist. If this is the save, + // then there's probably something really wrong (like a failed node-gyp build). If that's the case + // we should silently fail here and allow the error to raise in user-code so they can debug appropriately. + const sslCreds = await getGRPCInsecureCredential(frb).catch(NoOp); + const localAdminModule = require(adminResolution.resolution); - let hasInitializedFirestore = false; + adminModuleProxy = new Proxied(localAdminModule) + .when("initializeApp", (adminModuleTarget) => (opts: any, appName: any) => { + if (appName) { + new EmulatorLog("SYSTEM", "non-default-admin-app-used", "", { appName }).log(); + return adminModuleTarget.initializeApp(opts, appName); + } - /* - If we can't get sslCreds that means either grpc or grpc-js doesn't exist. If this is the save, - then there's probably something really wrong (like a failed node-gyp build). If that's the case - we should silently fail here and allow the error to raise in user-code so they can debug appropriately. - */ - const sslCreds = await getGRPCInsecureCredential(frb).catch(NoOp); - const initializeSettings = (userSettings: any) => { + const config = JSON.parse(process.env.FIREBASE_CONFIG || "{}"); + config.credential = makeFakeCredentials(); + + if (frb.ports.database) { + config.databaseURL = `http://localhost:${frb.ports.database}?ns=${frb.projectId}`; + new EmulatorLog("SYSTEM", `Overriding database URL: ${config.databaseURL}`, "").log(); + } + + const appOptions = { + ...config, + ...opts, + }; + const originalApp = adminModuleTarget.initializeApp(appOptions); + + new EmulatorLog("DEBUG", "default-admin-app-used", "", appOptions).log(); + logDebug("Intializing default app.", appOptions); + defaultApp = makeProxiedApp(frb, originalApp, sslCreds); + return defaultApp; + }) + .when("firestore", (target: any) => { + const adminFirestoreProxy = new Proxied(target.firestore).applied( + () => { + return defaultApp.firestore(); + } + ); + + return adminFirestoreProxy.finalize(); + }) + .finalize(); + + // Stub the admin module in the require cache + require.cache[adminResolution.resolution] = { + exports: adminModuleProxy, + }; + + logDebug("firebase-admin has been stubbed.", { + adminResolution, + }); + + return adminModuleProxy; +} + +function makeProxiedApp( + frb: FunctionsRuntimeBundle, + original: admin.app.App, + sslCreds: any +): admin.app.App { + const initializeFirestoreSettings = (firestoreTarget: any, userSettings: any) => { const isEnabled = isFeatureEnabled(frb, "admin_stubs"); if (!isEnabled) { if (!hasInitializedFirestore) { - app.firestore().settings(userSettings); + firestoreTarget.settings(userSettings); hasInitializedFirestore = true; } return; } if (!hasInitializedFirestore && frb.ports.firestore) { - app.firestore().settings({ + const emulatorSettings = { projectId: frb.projectId, port: frb.ports.firestore, servicePath: "localhost", @@ -520,7 +574,9 @@ async function InitializeFirebaseAdminStubs(frb: FunctionsRuntimeBundle): Promis Authorization: "Bearer owner", }, ...userSettings, - }); + }; + firestoreTarget.settings(emulatorSettings); + logDebug(`Initializing Firestore with custom settings.`, emulatorSettings); } else if (!frb.ports.firestore && frb.triggerId) { new EmulatorLog( "WARN", @@ -533,59 +589,27 @@ async function InitializeFirebaseAdminStubs(frb: FunctionsRuntimeBundle): Promis hasInitializedFirestore = true; }; - adminModuleProxy = new Proxied(localAdminModule) - .when("initializeApp", (adminModuleTarget) => (opts: any, appName: any) => { - if (appName) { - new EmulatorLog("SYSTEM", "non-default-admin-app-used", "", { appName }).log(); - return adminModuleTarget.initializeApp(opts, appName); - } - - const config = JSON.parse(process.env.FIREBASE_CONFIG || "{}"); - new EmulatorLog("SYSTEM", "default-admin-app-used", `config=${config}`).log(); - - config.credential = makeFakeCredentials(); - - if (frb.ports.database) { - config.databaseURL = `http://localhost:${frb.ports.database}?ns=${frb.projectId}`; - new EmulatorLog("SYSTEM", `Overriding database URL: ${config.databaseURL}`, "").log(); - } - - app = adminModuleTarget.initializeApp({ - ...config, - ...opts, - }); - return app; - }) - .when("firestore", (adminModuleTarget) => { - const proxied = new Proxied(adminModuleTarget.firestore); - return proxied - .applied(() => { - return new Proxied(adminModuleTarget.firestore()) - .when("settings", () => { - return (settings: any) => { - initializeSettings(settings); - }; - }) - .any((target, field) => { - initializeSettings({}); - return proxied.getOriginal(target, field); - }) - .finalize(); - }) - .finalize(); - }) - .finalize(); - - // Stub the admin module in the require cache - require.cache[adminResolution.resolution] = { - exports: adminModuleProxy, - }; - - new EmulatorLog("DEBUG", "runtime-status", "firebase-admin has been stubbed.", { - adminResolution, - }).log(); + const appProxy = new Proxied(original); + appProxy.when("firestore", (target: any) => { + const firestoreProxy = new Proxied(target.firestore); + return firestoreProxy + .applied(() => { + return new Proxied(target.firestore()) + .when("settings", (firestoreTarget) => { + return (settings: any) => { + initializeFirestoreSettings(firestoreTarget, settings); + }; + }) + .any((firestoreTarget, field) => { + initializeFirestoreSettings(firestoreTarget, {}); + return firestoreProxy.getOriginal(firestoreTarget, field); + }) + .finalize(); + }) + .finalize(); + }); - return adminModuleProxy; + return appProxy.finalize(); } /* @@ -617,17 +641,17 @@ async function InitializeFunctionsConfigHelper(functionsDir: string): Promise { - new EmulatorLog("DEBUG", "runtime-status", "config() parent accessed!", { + logDebug("config() parent accessed!", { parentKey, parentConfig, - }).log(); + }); return new Proxied(parentConfig[parentKey] || ({} as { [key: string]: any })) .any((childConfig, childKey) => { @@ -663,10 +687,10 @@ async function ProcessHTTPS(frb: FunctionsRuntimeBundle, trigger: EmulatedTrigge await new Promise((resolveEphemeralServer, rejectEphemeralServer) => { const handler = async (req: express.Request, res: express.Response) => { try { - new EmulatorLog("DEBUG", "runtime-status", `Ephemeral server used!`).log(); + logDebug(`Ephemeral server used!`); const func = trigger.getRawFunction(); - new EmulatorLog("DEBUG", "runtime-status", "Body" + (req as any).rawBody).log(); + logDebug("Body: " + (req as any).rawBody); res.on("finish", () => { instance.close(); resolveEphemeralServer(); @@ -725,11 +749,7 @@ async function ProcessBackground( new EmulatorLog("SYSTEM", "runtime-status", "ready").log(); const proto = frb.proto; - new EmulatorLog( - "DEBUG", - "runtime-status", - `ProcessBackground: proto=${JSON.stringify(proto)}` - ).log(); + logDebug("ProcessBackground", proto); // All formats of the payload should carry a "data" property. The "context" property does // not exist in all versions. Where it doesn't exist, context is everything besides data. @@ -740,11 +760,7 @@ async function ProcessBackground( // This is due to the fact that the Firestore emulator sends payloads in a newer // format than production firestore. if (context.resource && context.resource.name) { - new EmulatorLog( - "DEBUG", - "runtime-status", - `ProcessBackground: lifting resource.name from resource ${JSON.stringify(context.resource)}` - ).log(); + logDebug("ProcessBackground: lifting resource.name from resource", context.resource); context.resource = context.resource.name; } @@ -770,14 +786,14 @@ async function Run(func: () => Promise): Promise { console.log = log; - new EmulatorLog("DEBUG", "runtime-status", `Ephemeral server survived.`).log(); + logDebug(`Ephemeral server survived.`); if (caughtErr) { throw caughtErr; } } async function RunBackground(proto: any, func: CloudFunction): Promise { - new EmulatorLog("DEBUG", "runtime-status", `RunBackground: proto=${JSON.stringify(proto)}`).log(); + logDebug("RunBackground", proto); await Run(() => { return func(proto.data, proto.context); @@ -827,14 +843,18 @@ async function moduleResolutionDetective(frb: FunctionsRuntimeBundle, error: Err }).log(); } +function logDebug(msg: string, data?: any): void { + new EmulatorLog("DEBUG", "runtime-status", msg, data).log(); +} + async function main(): Promise { const serializedFunctionsRuntimeBundle = process.argv[2] || "{}"; const serializedFunctionTrigger = process.argv[3]; - new EmulatorLog("DEBUG", "runtime-status", "Functions runtime initialized.", { + logDebug("Functions runtime initialized.", { cwd: process.cwd(), node_version: process.versions.node, - }).log(); + }); const frb = JSON.parse(serializedFunctionsRuntimeBundle) as FunctionsRuntimeBundle; @@ -844,11 +864,7 @@ async function main(): Promise { }).log(); } - new EmulatorLog( - "DEBUG", - "runtime-status", - `Disabled runtime features: ${JSON.stringify(frb.disabled_features)}` - ).log(); + logDebug(`Disabled runtime features: ${JSON.stringify(frb.disabled_features)}`); const verified = await verifyDeveloperNodeModules(frb); if (!verified) { @@ -913,20 +929,16 @@ async function main(): Promise { ).log(); return; } else { - new EmulatorLog( - "DEBUG", - "runtime-status", - `Trigger "${frb.triggerId}" has been found, beginning invocation!` - ).log(); + logDebug(`Trigger "${frb.triggerId}" has been found, beginning invocation!`); } const trigger = triggers[frb.triggerId]; - new EmulatorLog("DEBUG", "runtime-status", "", trigger.definition).log(); + logDebug("", trigger.definition); const mode = trigger.definition.httpsTrigger ? "HTTPS" : "BACKGROUND"; - new EmulatorLog("DEBUG", "runtime-status", `Running ${frb.triggerId} in mode ${mode}`).log(); + logDebug(`Running ${frb.triggerId} in mode ${mode}`); - if (!app) { + if (!defaultApp) { adminModuleProxy.initializeApp(); new EmulatorLog("SYSTEM", "admin-auto-initialized", "").log(); } From 377b28618968b9f643b594469eb1d850ef4d5306 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Tue, 2 Jul 2019 11:21:29 -0700 Subject: [PATCH 3/5] Add new test --- src/emulator/functionsEmulator.ts | 4 +- src/emulator/functionsEmulatorRuntime.ts | 10 +++-- .../functionsEmulatorRuntime.spec.ts | 42 +++++++++++++++++++ 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index d8d7042b672..98a8f86cc52 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -388,9 +388,9 @@ You can probably fix this by running "npm install ${ break; case "DEBUG": if (log.data && log.data !== {}) { - EmulatorLogger.log("DEBUG", `${log.text} data=${JSON.stringify(log.data)}`); + EmulatorLogger.log("DEBUG", `[${log.type}] ${log.text} ${JSON.stringify(log.data)}`); } else { - EmulatorLogger.log("DEBUG", log.text); + EmulatorLogger.log("DEBUG", `[${log.type}] ${log.text}`); } break; case "INFO": diff --git a/src/emulator/functionsEmulatorRuntime.ts b/src/emulator/functionsEmulatorRuntime.ts index 545926f2752..5ac1ec8e570 100644 --- a/src/emulator/functionsEmulatorRuntime.ts +++ b/src/emulator/functionsEmulatorRuntime.ts @@ -521,10 +521,13 @@ async function InitializeFirebaseAdminStubs(frb: FunctionsRuntimeBundle): Promis new EmulatorLog("DEBUG", "default-admin-app-used", "", appOptions).log(); logDebug("Intializing default app.", appOptions); - defaultApp = makeProxiedApp(frb, originalApp, sslCreds); + defaultApp = proxyFirebaseApp(frb, originalApp, sslCreds); return defaultApp; }) .when("firestore", (target: any) => { + // When we call admin.firestore() we want to forward this on to the default app, + // but if you access something like admin.firestore.FieldValue we don't want to + // do anything. const adminFirestoreProxy = new Proxied(target.firestore).applied( () => { return defaultApp.firestore(); @@ -547,7 +550,7 @@ async function InitializeFirebaseAdminStubs(frb: FunctionsRuntimeBundle): Promis return adminModuleProxy; } -function makeProxiedApp( +function proxyFirebaseApp( frb: FunctionsRuntimeBundle, original: admin.app.App, sslCreds: any @@ -576,7 +579,8 @@ function makeProxiedApp( ...userSettings, }; firestoreTarget.settings(emulatorSettings); - logDebug(`Initializing Firestore with custom settings.`, emulatorSettings); + + new EmulatorLog("DEBUG", "set-firestore-settings", "", emulatorSettings).log(); } else if (!frb.ports.firestore && frb.triggerId) { new EmulatorLog( "WARN", diff --git a/src/test/emulators/functionsEmulatorRuntime.spec.ts b/src/test/emulators/functionsEmulatorRuntime.spec.ts index 7aa3aace26f..fbb0fe8efe3 100644 --- a/src/test/emulators/functionsEmulatorRuntime.spec.ts +++ b/src/test/emulators/functionsEmulatorRuntime.spec.ts @@ -187,6 +187,48 @@ describe("FunctionsEmulator-Runtime", () => { expect(logs["function-log"]).to.eq(1); }).timeout(TIMEOUT_MED); + it("should provide a stubbed Firestore through admin.firestore()", async () => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onCreate, () => { + const admin = require("firebase-admin"); + admin.initializeApp(); + const firestore = admin.firestore(); + + return { + function_id: require("firebase-functions") + .firestore.document("test/test") + .onCreate(async () => { + // Need to use the Firestore object in order to trigger init. + const ref = firestore.collection("foo").doc("bar"); + return true; + }), + }; + }); + + const logs = await _countLogEntries(runtime); + expect(logs["set-firestore-settings"]).to.eq(1); + }).timeout(TIMEOUT_MED); + + it("should provide a stubbed Firestore through app.firestore()", async () => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onCreate, () => { + const admin = require("firebase-admin"); + const app = admin.initializeApp(); + const firestore = app.firestore(); + + return { + function_id: require("firebase-functions") + .firestore.document("test/test") + .onCreate(async () => { + // Need to use the Firestore object in order to trigger init. + const ref = firestore.collection("foo").doc("bar"); + return true; + }), + }; + }); + + const logs = await _countLogEntries(runtime); + expect(logs["set-firestore-settings"]).to.eq(1); + }).timeout(TIMEOUT_MED); + it("should redirect Firestore write to emulator", async () => { const onRequestCopy = _.cloneDeep( FunctionRuntimeBundles.onRequest From 2487de243386ddb61f9919c07c0e062648ef38b6 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Tue, 2 Jul 2019 11:25:08 -0700 Subject: [PATCH 4/5] Changelog --- changelog.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 39a45b5065c..d584e51d207 100644 --- a/changelog.txt +++ b/changelog.txt @@ -2,4 +2,5 @@ * Make `functions:shell` respect the `--port` argument. * Improve error message when `firebase serve` can't acquire the right port. * Allow running the Firestore and RTDB emulators without a configuration. -* Allow the Firestore emulator to give more information about invalid rulesets. \ No newline at end of file +* Allow the Firestore emulator to give more information about invalid rulesets. +* Fixes a bug where `admin.firestore()` and `app.firestore()` behaved differently. \ No newline at end of file From a3c289d25086eab2bc8235a120e9aed65789a839 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Tue, 2 Jul 2019 15:27:33 -0700 Subject: [PATCH 5/5] Some comments and types --- src/emulator/functionsEmulatorRuntime.ts | 84 ++++++++++++++---------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/src/emulator/functionsEmulatorRuntime.ts b/src/emulator/functionsEmulatorRuntime.ts index 5ac1ec8e570..9e23a4b4fb5 100644 --- a/src/emulator/functionsEmulatorRuntime.ts +++ b/src/emulator/functionsEmulatorRuntime.ts @@ -16,7 +16,6 @@ import * as admin from "firebase-admin"; import * as bodyParser from "body-parser"; import { URL } from "url"; import * as _ from "lodash"; -import { Message } from "firebase-functions/lib/providers/pubsub"; let defaultApp: admin.app.App; let adminModuleProxy: typeof admin; @@ -44,14 +43,6 @@ async function requireResolveAsync( return require.resolve(moduleName, opts); } -function isConstructor(obj: any): boolean { - return !!obj.prototype && !!obj.prototype.constructor.name; -} - -function isExists(obj: any): boolean { - return obj !== undefined; -} - /** * See admin.credential.Credential. */ @@ -89,6 +80,10 @@ interface ModuleVersion { patch: number; } +interface ProxyTarget extends Object { + [key: string]: any; +} + /* This helper is used to create mocks for Firebase SDKs. It simplifies creation of Proxy objects by allowing us to easily overide some or all of an objects methods. When placed back into require's @@ -102,21 +97,21 @@ interface ModuleVersion { obj.value == 1; obj.incremented == 2; */ -class Proxied { - proxy: any; - private anyValue?: (target: any, key: string) => any; +class Proxied { + proxy: T; + private anyValue?: (target: T, key: string) => any; private appliedValue?: () => any; private rewrites: { - [key: string]: (target: any, key: string) => any; + [key: string]: (target: T, key: string) => any; } = {}; - constructor(private original: any) { - /* - When initialized an original object is passed. This object is supplied to both .when() - and .any() functions so the original value of the object is accessible. When no - .any() is provided, the original value of the object is returned when the field - key does not match any known rewrite. - */ + /** + * When initialized an original object is passed. This object is supplied to both .when() + * and .any() functions so the original value of the object is accessible. When no + * .any() is provided, the original value of the object is returned when the field + * key does not match any known rewrite. + */ + constructor(private original: T) { this.proxy = new Proxy(this.original, { get: (target, key) => { key = key.toString(); @@ -143,7 +138,7 @@ class Proxied { /** * Calling .when("a", () => "b") will rewrite obj["a"] to be equal to "b" */ - when(key: string, value: (target: any, key: string) => any): Proxied { + when(key: string, value: (target: T, key: string) => any): Proxied { this.rewrites[key] = value; return this as Proxied; } @@ -151,7 +146,7 @@ class Proxied { /** * Calling .any(() => "b") will rewrite all fields on obj to be equal to "b" */ - any(value: (target: any, key: string) => any): Proxied { + any(value: (target: T, key: string) => any): Proxied { this.anyValue = value; return this as Proxied; } @@ -164,25 +159,42 @@ class Proxied { return this as Proxied; } - getOriginal(target: any, key: string): any { + /** + * Gets a property from the original object. + */ + getOriginal(target: T, key: string): any { const value = target[key]; - if (!isExists(value)) { + if (!this.isExists(value)) { return undefined; - } else if (isConstructor(value) || typeof value !== "function") { + } else if (this.isConstructor(value) || typeof value !== "function") { return value; } else { return value.bind(target); } } + /** + * Run the original target. + */ applyOriginal(target: any, thisArg: any, argArray: any[]): any { return target.apply(thisArg, argArray); } + /** + * Return the final proxied object. + */ finalize(): T { return this.proxy as T; } + + private isConstructor(obj: any): boolean { + return !!obj.prototype && !!obj.prototype.constructor.name; + } + + private isExists(obj: any): boolean { + return obj !== undefined; + } } async function resolveDeveloperNodeModule( @@ -499,7 +511,7 @@ async function InitializeFirebaseAdminStubs(frb: FunctionsRuntimeBundle): Promis const localAdminModule = require(adminResolution.resolution); adminModuleProxy = new Proxied(localAdminModule) - .when("initializeApp", (adminModuleTarget) => (opts: any, appName: any) => { + .when("initializeApp", (adminModuleTarget) => (opts?: admin.AppOptions, appName?: string) => { if (appName) { new EmulatorLog("SYSTEM", "non-default-admin-app-used", "", { appName }).log(); return adminModuleTarget.initializeApp(opts, appName); @@ -524,15 +536,15 @@ async function InitializeFirebaseAdminStubs(frb: FunctionsRuntimeBundle): Promis defaultApp = proxyFirebaseApp(frb, originalApp, sslCreds); return defaultApp; }) - .when("firestore", (target: any) => { + .when("firestore", (adminModuleTarget) => { // When we call admin.firestore() we want to forward this on to the default app, // but if you access something like admin.firestore.FieldValue we don't want to // do anything. - const adminFirestoreProxy = new Proxied(target.firestore).applied( - () => { - return defaultApp.firestore(); - } - ); + const adminFirestoreProxy = new Proxied( + adminModuleTarget.firestore + ).applied(() => { + return defaultApp.firestore(); + }); return adminFirestoreProxy.finalize(); }) @@ -595,6 +607,8 @@ function proxyFirebaseApp( const appProxy = new Proxied(original); appProxy.when("firestore", (target: any) => { + // TODO: this 'typeof admin.firestore' should probably be 'Firestore' from @google-cloud/firestore + // but I can't get all the type checking to play nice. const firestoreProxy = new Proxied(target.firestore); return firestoreProxy .applied(() => { @@ -918,10 +932,8 @@ async function main(): Promise { new EmulatorLog("SYSTEM", "triggers-parsed", "", { triggers, triggerDefinitions }).log(); if (!frb.triggerId) { - /* - This is a purely diagnostic call, it's used as a check to make sure developer code compiles and runs as - expected, so we don't have any function to invoke. - */ + // This is a purely diagnostic call, it's used as a check to make sure developer code compiles and runs as + // expected, so we don't have any function to invoke. return; }