src/loader.ts (115 lines of code) (raw):

// Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // loader.ts /** * This package contains the logic to load user's function. * @packageDocumentation */ import * as path from 'path'; import * as semver from 'semver'; import {pathToFileURL} from 'url'; import {HandlerFunction} from './functions'; import {SignatureType} from './types'; import {getRegisteredFunction} from './function_registry'; // Dynamic import function required to load user code packaged as an // ES module is only available on Node.js v13.2.0 and up. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#browser_compatibility // Exported for testing. export const MIN_NODE_VERSION_ESMODULES = '13.2.0'; /** * Determines whether the given module is an ES module. * * Implements "algorithm" described at: * https://nodejs.org/api/packages.html#packages_type * * In words: * 1. A module with .mjs extension is an ES module. * 2. A module with .clj extension is not an ES module. * 3. A module with .js extensions where the nearest package.json's * with "type": "module" is an ES module. * 4. Otherwise, it is not an ES module. * * @returns {Promise<boolean>} True if module is an ES module. */ async function isEsModule(modulePath: string): Promise<boolean> { const ext = path.extname(modulePath); if (ext === '.mjs') { return true; } if (ext === '.cjs') { return false; } const {readPackageUp} = await dynamicImport('read-package-up'); const pkg = await readPackageUp({ cwd: path.dirname(modulePath), normalize: false, }); // If package.json specifies type as 'module', it's an ES module. return pkg?.packageJson.type === 'module'; } /** * Dynamically load import function to prevent TypeScript from * transpiling into a require. * * See https://github.com/microsoft/TypeScript/issues/43329. */ const dynamicImport = new Function( 'modulePath', 'return import(modulePath)', // eslint-disable-next-line @typescript-eslint/no-explicit-any ) as (modulePath: string) => Promise<any>; /** * Returns user's function from function file. * Returns null if function can't be retrieved. * @return User's function or null. */ export async function getUserFunction( codeLocation: string, functionTarget: string, signatureType: SignatureType, ): Promise<{ userFunction: HandlerFunction; signatureType: SignatureType; } | null> { try { const functionModulePath = getFunctionModulePath(codeLocation); if (functionModulePath === null) { console.error( `Provided code location '${codeLocation}' is not a loadable module.` + '\nDid you specify the correct location for the module defining ' + 'your function?', ); return null; } let functionModule; const esModule = await isEsModule(functionModulePath); if (esModule) { if (semver.lt(process.version, MIN_NODE_VERSION_ESMODULES)) { console.error( `Cannot load ES Module on Node.js ${process.version}. ` + `Please upgrade to Node.js v${MIN_NODE_VERSION_ESMODULES} and up.`, ); return null; } // Resolve module path to file:// URL. Required for windows support. const fpath = pathToFileURL(functionModulePath); functionModule = await dynamicImport(fpath.href); } else { functionModule = require(functionModulePath); } // If the customer declaratively registered a function matching the target // return that. const registeredFunction = getRegisteredFunction(functionTarget); if (registeredFunction) { return registeredFunction; } const userFunction = functionTarget .split('.') .reduce((code, functionTargetPart) => { if (typeof code === 'undefined') { return undefined; } else { return code[functionTargetPart]; } }, functionModule); if (typeof userFunction === 'undefined') { console.error( `Function '${functionTarget}' is not defined in the provided ` + 'module.\nDid you specify the correct target function to execute?', ); return null; } if (typeof userFunction !== 'function') { console.error( `'${functionTarget}' needs to be of type function. Got: ` + `${typeof userFunction}`, ); return null; } return {userFunction: userFunction as HandlerFunction, signatureType}; } catch (ex) { const err: Error = <Error>ex; let additionalHint: string; // TODO: this should be done based on ex.code rather than string matching. if (err.stack && err.stack.includes('Cannot find module')) { additionalHint = 'Did you list all required modules in the package.json ' + 'dependencies?\n'; } else { additionalHint = 'Is there a syntax error in your code?\n'; } console.error( `Provided module can't be loaded.\n${additionalHint}` + `Detailed stack trace: ${err.stack}`, ); return null; } } /** * Returns resolved path to the module containing the user function. * Returns null if the module can not be identified. * @param codeLocation - Directory with user's code * @return Resolved path or null. */ function getFunctionModulePath(codeLocation: string): string | null { try { return require.resolve(codeLocation); } catch (ex) { // Ignore exception, this means the function was not found here. } try { return require.resolve(codeLocation + '/function.js'); } catch (ex) { // Ignore exception, this means the function was not found here. } return null; }