No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

extensionsEmulator.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.ExtensionsEmulator = void 0;
  4. const clc = require("colorette");
  5. const spawn = require("cross-spawn");
  6. const fs = require("fs-extra");
  7. const os = require("os");
  8. const path = require("path");
  9. const Table = require("cli-table");
  10. const planner = require("../deploy/extensions/planner");
  11. const ensureApiEnabled_1 = require("../ensureApiEnabled");
  12. const error_1 = require("../error");
  13. const optionsHelper_1 = require("../extensions/emulator/optionsHelper");
  14. const refs_1 = require("../extensions/refs");
  15. const shortenUrl_1 = require("../shortenUrl");
  16. const constants_1 = require("./constants");
  17. const download_1 = require("./download");
  18. const emulatorLogger_1 = require("./emulatorLogger");
  19. const validation_1 = require("./extensions/validation");
  20. const registry_1 = require("./registry");
  21. const types_1 = require("./types");
  22. class ExtensionsEmulator {
  23. constructor(args) {
  24. this.want = [];
  25. this.backends = [];
  26. this.logger = emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.EXTENSIONS);
  27. this.pendingDownloads = new Map();
  28. this.args = args;
  29. }
  30. start() {
  31. this.logger.logLabeled("DEBUG", "Extensions", "Started Extensions emulator, this is a noop.");
  32. return Promise.resolve();
  33. }
  34. stop() {
  35. this.logger.logLabeled("DEBUG", "Extensions", "Stopping Extensions emulator, this is a noop.");
  36. return Promise.resolve();
  37. }
  38. connect() {
  39. this.logger.logLabeled("DEBUG", "Extensions", "Connecting Extensions emulator, this is a noop.");
  40. return Promise.resolve();
  41. }
  42. getInfo() {
  43. const functionsEmulator = registry_1.EmulatorRegistry.get(types_1.Emulators.FUNCTIONS);
  44. if (!functionsEmulator) {
  45. throw new error_1.FirebaseError("Extensions Emulator is running but Functions emulator is not. This should never happen.");
  46. }
  47. return Object.assign(Object.assign({}, functionsEmulator.getInfo()), { name: this.getName() });
  48. }
  49. getName() {
  50. return types_1.Emulators.EXTENSIONS;
  51. }
  52. async readManifest() {
  53. var _a;
  54. this.want = await planner.want({
  55. projectId: this.args.projectId,
  56. projectNumber: this.args.projectNumber,
  57. aliases: (_a = this.args.aliases) !== null && _a !== void 0 ? _a : [],
  58. projectDir: this.args.projectDir,
  59. extensions: this.args.extensions,
  60. emulatorMode: true,
  61. });
  62. }
  63. async ensureSourceCode(instance) {
  64. if (instance.localPath) {
  65. if (!this.hasValidSource({ path: instance.localPath, extTarget: instance.localPath })) {
  66. throw new error_1.FirebaseError(`Tried to emulate local extension at ${instance.localPath}, but it was missing required files.`);
  67. }
  68. return path.resolve(instance.localPath);
  69. }
  70. else if (instance.ref) {
  71. const ref = (0, refs_1.toExtensionVersionRef)(instance.ref);
  72. const cacheDir = process.env.FIREBASE_EXTENSIONS_CACHE_PATH ||
  73. path.join(os.homedir(), ".cache", "firebase", "extensions");
  74. const sourceCodePath = path.join(cacheDir, ref);
  75. if (this.pendingDownloads.get(ref)) {
  76. await this.pendingDownloads.get(ref);
  77. }
  78. if (!this.hasValidSource({ path: sourceCodePath, extTarget: ref })) {
  79. const promise = this.downloadSource(instance, ref, sourceCodePath);
  80. this.pendingDownloads.set(ref, promise);
  81. await promise;
  82. }
  83. return sourceCodePath;
  84. }
  85. else {
  86. throw new error_1.FirebaseError("Tried to emulate an extension instance without a ref or localPath. This should never happen.");
  87. }
  88. }
  89. async downloadSource(instance, ref, sourceCodePath) {
  90. const extensionVersion = await planner.getExtensionVersion(instance);
  91. await (0, download_1.downloadExtensionVersion)(ref, extensionVersion.sourceDownloadUri, sourceCodePath);
  92. this.installAndBuildSourceCode(sourceCodePath);
  93. }
  94. hasValidSource(args) {
  95. const requiredFiles = [
  96. "./extension.yaml",
  97. "./functions/package.json",
  98. "./functions/node_modules",
  99. ];
  100. if (!fs.existsSync(args.path)) {
  101. return false;
  102. }
  103. for (const requiredFile of requiredFiles) {
  104. const f = path.join(args.path, requiredFile);
  105. if (!fs.existsSync(f)) {
  106. this.logger.logLabeled("BULLET", "extensions", `Detected invalid source code for ${args.extTarget}, expected to find ${f}`);
  107. return false;
  108. }
  109. }
  110. this.logger.logLabeled("DEBUG", "extensions", `Source code valid for ${args.extTarget}`);
  111. return true;
  112. }
  113. installAndBuildSourceCode(sourceCodePath) {
  114. this.logger.logLabeled("DEBUG", "Extensions", `Running "npm install" for ${sourceCodePath}`);
  115. const functionsDirectory = path.resolve(sourceCodePath, "functions");
  116. const npmInstall = spawn.sync("npm", ["install"], {
  117. encoding: "utf8",
  118. cwd: functionsDirectory,
  119. });
  120. if (npmInstall.error) {
  121. throw npmInstall.error;
  122. }
  123. this.logger.logLabeled("DEBUG", "Extensions", `Finished "npm install" for ${sourceCodePath}`);
  124. this.logger.logLabeled("DEBUG", "Extensions", `Running "npm run gcp-build" for ${sourceCodePath}`);
  125. const npmRunGCPBuild = spawn.sync("npm", ["run", "gcp-build"], {
  126. encoding: "utf8",
  127. cwd: functionsDirectory,
  128. });
  129. if (npmRunGCPBuild.error) {
  130. throw npmRunGCPBuild.error;
  131. }
  132. this.logger.logLabeled("DEBUG", "Extensions", `Finished "npm run gcp-build" for ${sourceCodePath}`);
  133. }
  134. async getExtensionBackends() {
  135. await this.readManifest();
  136. await this.checkAndWarnAPIs(this.want);
  137. this.backends = await Promise.all(this.want.map((i) => {
  138. return this.toEmulatableBackend(i);
  139. }));
  140. return this.backends;
  141. }
  142. async toEmulatableBackend(instance) {
  143. const extensionDir = await this.ensureSourceCode(instance);
  144. const functionsDir = path.join(extensionDir, "functions");
  145. const env = Object.assign(this.autoPopulatedParams(instance), instance.params);
  146. const { extensionTriggers, nodeMajorVersion, nonSecretEnv, secretEnvVariables } = await (0, optionsHelper_1.getExtensionFunctionInfo)(instance, env);
  147. const emulatableBackend = {
  148. functionsDir,
  149. env: nonSecretEnv,
  150. codebase: instance.instanceId,
  151. secretEnv: secretEnvVariables,
  152. predefinedTriggers: extensionTriggers,
  153. nodeMajorVersion: nodeMajorVersion,
  154. extensionInstanceId: instance.instanceId,
  155. };
  156. if (instance.ref) {
  157. emulatableBackend.extension = await planner.getExtension(instance);
  158. emulatableBackend.extensionVersion = await planner.getExtensionVersion(instance);
  159. }
  160. else if (instance.localPath) {
  161. emulatableBackend.extensionSpec = await planner.getExtensionSpec(instance);
  162. }
  163. return emulatableBackend;
  164. }
  165. autoPopulatedParams(instance) {
  166. var _a;
  167. const projectId = this.args.projectId;
  168. return {
  169. PROJECT_ID: projectId !== null && projectId !== void 0 ? projectId : "",
  170. EXT_INSTANCE_ID: instance.instanceId,
  171. DATABASE_INSTANCE: projectId !== null && projectId !== void 0 ? projectId : "",
  172. DATABASE_URL: `https://${projectId}.firebaseio.com`,
  173. STORAGE_BUCKET: `${projectId}.appspot.com`,
  174. ALLOWED_EVENT_TYPES: instance.allowedEventTypes ? instance.allowedEventTypes.join(",") : "",
  175. EVENTARC_CHANNEL: (_a = instance.eventarcChannel) !== null && _a !== void 0 ? _a : "",
  176. EVENTARC_CLOUD_EVENT_SOURCE: `projects/${projectId}/instances/${instance.instanceId}`,
  177. };
  178. }
  179. async checkAndWarnAPIs(instances) {
  180. const apisToWarn = await (0, validation_1.getUnemulatedAPIs)(this.args.projectId, instances);
  181. if (apisToWarn.length) {
  182. const table = new Table({
  183. head: [
  184. "API Name",
  185. "Instances using this API",
  186. `Enabled on ${this.args.projectId}`,
  187. `Enable this API`,
  188. ],
  189. style: { head: ["yellow"] },
  190. });
  191. for (const apiToWarn of apisToWarn) {
  192. const enablementUri = await (0, shortenUrl_1.shortenUrl)((0, ensureApiEnabled_1.enableApiURI)(this.args.projectId, apiToWarn.apiName));
  193. table.push([
  194. apiToWarn.apiName,
  195. apiToWarn.instanceIds,
  196. apiToWarn.enabled ? "Yes" : "No",
  197. apiToWarn.enabled ? "" : clc.bold(clc.underline(enablementUri)),
  198. ]);
  199. }
  200. if (constants_1.Constants.isDemoProject(this.args.projectId)) {
  201. this.logger.logLabeled("WARN", "Extensions", "The following Extensions make calls to Google Cloud APIs that do not have Emulators. " +
  202. `${clc.bold(this.args.projectId)} is a demo project, so these Extensions may not work as expected.\n` +
  203. table.toString());
  204. }
  205. else {
  206. this.logger.logLabeled("WARN", "Extensions", "The following Extensions make calls to Google Cloud APIs that do not have Emulators. " +
  207. `These calls will go to production Google Cloud APIs which may have real effects on ${clc.bold(this.args.projectId)}.\n` +
  208. table.toString());
  209. }
  210. }
  211. }
  212. filterUnemulatedTriggers(options, backends) {
  213. let foundUnemulatedTrigger = false;
  214. const filteredBackends = backends.filter((backend) => {
  215. const unemulatedServices = (0, validation_1.checkForUnemulatedTriggerTypes)(backend, options);
  216. if (unemulatedServices.length) {
  217. foundUnemulatedTrigger = true;
  218. const msg = ` ignored becuase it includes ${unemulatedServices.join(", ")} triggered functions, and the ${unemulatedServices.join(", ")} emulator does not exist or is not running.`;
  219. this.logger.logLabeled("WARN", `extensions[${backend.extensionInstanceId}]`, msg);
  220. }
  221. return unemulatedServices.length === 0;
  222. });
  223. if (foundUnemulatedTrigger) {
  224. const msg = "No Cloud Functions for these instances will be emulated, because partially emulating an Extension can lead to unexpected behavior. ";
  225. this.logger.log("WARN", msg);
  226. }
  227. return filteredBackends;
  228. }
  229. extensionDetailsUILink(backend) {
  230. if (!registry_1.EmulatorRegistry.isRunning(types_1.Emulators.UI) || !backend.extensionInstanceId) {
  231. return "";
  232. }
  233. const uiUrl = registry_1.EmulatorRegistry.url(types_1.Emulators.UI);
  234. uiUrl.pathname = `/${types_1.Emulators.EXTENSIONS}/${backend.extensionInstanceId}`;
  235. return clc.underline(clc.bold(uiUrl.toString()));
  236. }
  237. extensionsInfoTable(options) {
  238. var _a;
  239. const filtedBackends = this.filterUnemulatedTriggers(options, this.backends);
  240. const uiRunning = registry_1.EmulatorRegistry.isRunning(types_1.Emulators.UI);
  241. const tableHead = ["Extension Instance Name", "Extension Ref"];
  242. if (uiRunning) {
  243. tableHead.push("View in Emulator UI");
  244. }
  245. const table = new Table({ head: tableHead, style: { head: ["yellow"] } });
  246. for (const b of filtedBackends) {
  247. if (b.extensionInstanceId) {
  248. const tableEntry = [b.extensionInstanceId, ((_a = b.extensionVersion) === null || _a === void 0 ? void 0 : _a.ref) || "Local Extension"];
  249. if (uiRunning)
  250. tableEntry.push(this.extensionDetailsUILink(b));
  251. table.push(tableEntry);
  252. }
  253. }
  254. return table.toString();
  255. }
  256. }
  257. exports.ExtensionsEmulator = ExtensionsEmulator;