From d507b1c76dc8164a1743f3608f5b1ab9e606439b Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 28 Jun 2021 20:16:34 +0900 Subject: [PATCH] Add support ES modules for Firebase Functions (#3485) \To import modules packaged as ES module, we have to use import instead of require. Both the emulator and function deploy code hardcodes require to load user's function code and ends up throwing ERR_REQUIRE_ESM when given ES modules. We will now try using import when require fails with an ERR_REQUIRE_ESM error. This is a bit lazy - we could've detected whether given module is ES module vs CommonJS and dispatch require vs import appropriately - but I think it simplifies the script while still being correct. --- CHANGELOG.md | 1 + .../functions/runtimes/node/triggerParser.js | 25 +++++++++++++++++-- src/emulator/functionsEmulatorRuntime.ts | 19 ++++++++++++-- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d1..0e71b404de7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Support loading Firebase Functions packaged as an ES module. (#3485) diff --git a/src/deploy/functions/runtimes/node/triggerParser.js b/src/deploy/functions/runtimes/node/triggerParser.js index 0a488027ff5c..ff1bdd26bafb 100644 --- a/src/deploy/functions/runtimes/node/triggerParser.js +++ b/src/deploy/functions/runtimes/node/triggerParser.js @@ -7,7 +7,28 @@ var EXIT = function () { process.exit(0); }; -(function () { +/** + * Dynamically load import function to prevent TypeScript from + * transpiling into a require. + * + * See https://github.com/microsoft/TypeScript/issues/43329. + */ +// eslint-disable-next-line @typescript-eslint/no-implied-eval +const dynamicImport = new Function("modulePath", "return import(modulePath)"); + +async function loadModule(packageDir) { + try { + return require(packageDir); + } catch (e) { + if (e.code === "ERR_REQUIRE_ESM") { + const mod = await dynamicImport(require.resolve(packageDir)); + return mod; + } + throw e; + } +} + +(async function () { if (!process.send) { console.warn("Could not parse function triggers (process.send === undefined)."); process.exit(1); @@ -23,7 +44,7 @@ var EXIT = function () { var mod; var triggers = []; try { - mod = require(packageDir); + mod = await loadModule(packageDir); } catch (e) { if (e.code === "MODULE_NOT_FOUND") { process.send( diff --git a/src/emulator/functionsEmulatorRuntime.ts b/src/emulator/functionsEmulatorRuntime.ts index ac4e98842dee..9b99530e9a99 100644 --- a/src/emulator/functionsEmulatorRuntime.ts +++ b/src/emulator/functionsEmulatorRuntime.ts @@ -27,6 +27,15 @@ import * as _ from "lodash"; let triggers: EmulatedTriggerMap | undefined; let developerPkgJSON: PackageJSON | undefined; +/** + * Dynamically load import function to prevent TypeScript from + * transpiling into a require. + * + * See https://github.com/microsoft/TypeScript/issues/43329. + */ +// eslint-disable-next-line @typescript-eslint/no-implied-eval +const dynamicImport = new Function("modulePath", "return import(modulePath)"); + function isFeatureEnabled( frb: FunctionsRuntimeBundle, feature: keyof FunctionsRuntimeFeatures @@ -567,6 +576,7 @@ async function initializeFirebaseAdminStubs(frb: FunctionsRuntimeBundle): Promis // Stub the admin module in the require cache require.cache[adminResolution.resolution] = { exports: proxiedAdminModule, + path: path.dirname(adminResolution.resolution), }; logDebug("firebase-admin has been stubbed.", { @@ -776,6 +786,7 @@ async function initializeFunctionsConfigHelper(frb: FunctionsRuntimeBundle): Pro // Stub the functions module in the require cache require.cache[functionsResolution.resolution] = { exports: proxiedFunctionsModule, + path: path.dirname(functionsResolution.resolution), }; logDebug("firebase-functions has been stubbed.", { @@ -1065,8 +1076,12 @@ async function initializeRuntime( try { triggerModule = require(frb.cwd); } catch (err) { - await moduleResolutionDetective(frb, err); - return; + if (err.code !== "ERR_REQUIRE_ESM") { + await moduleResolutionDetective(frb, err); + return; + } + // tslint:disable:no-unsafe-assignment + triggerModule = await dynamicImport(require.resolve(frb.cwd)); } } if (extensionTriggers) {