serverless-spy
Version:
CDK-based library for writing elegant integration tests on AWS serverless architecture and an additional web console to monitor events in real time.
322 lines (281 loc) • 9.82 kB
JavaScript
/**
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* This module defines the functions for loading the user's code as specified
* in a handler string.
*/
;
const path = require('path');
const fs = require('fs');
const {
HandlerNotFound,
MalformedHandlerName,
ImportModuleError,
UserCodeSyntaxError,
} = require('./Errors.js');
const { verbose } = require('./VerboseLog.js').logger('LOADER');
const { HttpResponseStream } = require('./HttpResponseStream');
const FUNCTION_EXPR = /^([^.]*)\.(.*)$/;
const RELATIVE_PATH_SUBSTRING = '..';
const HANDLER_STREAMING = Symbol.for('aws.lambda.runtime.handler.streaming');
const HANDLER_HIGHWATERMARK = Symbol.for(
'aws.lambda.runtime.handler.streaming.highWaterMark'
);
const STREAM_RESPONSE = 'response';
// `awslambda.streamifyResponse` function is provided by default.
const NoGlobalAwsLambda =
process.env['AWS_LAMBDA_NODEJS_NO_GLOBAL_AWSLAMBDA'] === '1' ||
process.env['AWS_LAMBDA_NODEJS_NO_GLOBAL_AWSLAMBDA'] === 'true';
/**
* Break the full handler string into two pieces, the module root and the actual
* handler string.
* Given './somepath/something/module.nestedobj.handler' this returns
* ['./somepath/something', 'module.nestedobj.handler']
*/
function _moduleRootAndHandler(fullHandlerString) {
let handlerString = path.basename(fullHandlerString);
let moduleRoot = fullHandlerString.substring(
0,
fullHandlerString.indexOf(handlerString)
);
return [moduleRoot, handlerString];
}
/**
* Split the handler string into two pieces: the module name and the path to
* the handler function.
*/
function _splitHandlerString(handler) {
let match = handler.match(FUNCTION_EXPR);
if (!match || match.length != 3) {
throw new MalformedHandlerName('Bad handler');
}
return [match[1], match[2]]; // [module, function-path]
}
/**
* Resolve the user's handler function from the module.
*/
function _resolveHandler(object, nestedProperty) {
return nestedProperty.split('.').reduce((nested, key) => {
return nested && nested[key];
}, object);
}
function _tryRequireFile(file, extension) {
const path = file + (extension || '');
verbose('Try loading as commonjs:', path);
return fs.existsSync(path) ? require(path) : undefined;
}
async function _tryAwaitImport(file, extension) {
const path = file + (extension || '');
verbose('Try loading as esmodule:', path);
if (fs.existsSync(path)) {
return await import(path);
}
return undefined;
}
function _hasFolderPackageJsonTypeModule(folder) {
// Check if package.json exists, return true if type === "module" in package json.
// If there is no package.json, and there is a node_modules, return false.
// Check parent folder otherwise, if there is one.
if (folder.endsWith('/node_modules')) {
return false;
}
const pj = path.join(folder, '/package.json');
if (fs.existsSync(pj)) {
try {
const pkg = JSON.parse(fs.readFileSync(pj));
if (pkg) {
if (pkg.type === 'module') {
verbose('type: module detected in', pj);
return true;
} else {
verbose('type: module not detected in', pj);
return false;
}
}
} catch (e) {
console.warn(
`${pj} cannot be read, it will be ignored for ES module detection purposes.`,
e
);
return false;
}
}
if (folder === '/') {
// We have reached root without finding either a package.json or a node_modules.
return false;
}
return _hasFolderPackageJsonTypeModule(path.resolve(folder, '..'));
}
function _hasPackageJsonTypeModule(file) {
// File must have a .js extension
const jsPath = file + '.js';
return fs.existsSync(jsPath)
? _hasFolderPackageJsonTypeModule(path.resolve(path.dirname(jsPath)))
: false;
}
/**
* Attempt to load the user's module.
* Attempts to directly resolve the module relative to the application root,
* then falls back to the more general require().
*/
async function _tryRequire(appRoot, moduleRoot, module) {
verbose(
'Try loading as commonjs: ',
module,
' with paths: ,',
appRoot,
moduleRoot
);
const lambdaStylePath = path.resolve(appRoot, moduleRoot, module);
// Extensionless files are loaded via require.
const extensionless = _tryRequireFile(lambdaStylePath);
if (extensionless) {
return extensionless;
}
// If package.json type != module, .js files are loaded via require.
const pjHasModule = _hasPackageJsonTypeModule(lambdaStylePath);
if (!pjHasModule) {
const loaded = _tryRequireFile(lambdaStylePath, '.js');
if (loaded) {
return loaded;
}
}
// If still not loaded, try .js, .mjs, and .cjs in that order.
// Files ending with .js are loaded as ES modules when the nearest parent package.json
// file contains a top-level field "type" with a value of "module".
// https://nodejs.org/api/packages.html#packages_type
const loaded =
(pjHasModule && (await _tryAwaitImport(lambdaStylePath, '.js'))) ||
(await _tryAwaitImport(lambdaStylePath, '.mjs')) ||
_tryRequireFile(lambdaStylePath, '.cjs');
if (loaded) {
return loaded;
}
verbose(
'Try loading as commonjs: ',
module,
' with path(s): ',
appRoot,
moduleRoot
);
// Why not just require(module)?
// Because require() is relative to __dirname, not process.cwd(). And the
// runtime implementation is not located in /var/task
// This won't work (yet) for esModules as import.meta.resolve is still experimental
// See: https://nodejs.org/api/esm.html#esm_import_meta_resolve_specifier_parent
const nodeStylePath = require.resolve(module, {
paths: [appRoot, moduleRoot],
});
return require(nodeStylePath);
}
/**
* Load the user's application or throw a descriptive error.
* @throws Runtime errors in two cases
* 1 - UserCodeSyntaxError if there's a syntax error while loading the module
* 2 - ImportModuleError if the module cannot be found
*/
async function _loadUserApp(appRoot, moduleRoot, module) {
if (!NoGlobalAwsLambda) {
globalThis.awslambda = {
streamifyResponse: (handler, options) => {
handler[HANDLER_STREAMING] = STREAM_RESPONSE;
if (typeof options?.highWaterMark === 'number') {
handler[HANDLER_HIGHWATERMARK] = parseInt(options.highWaterMark);
}
return handler;
},
HttpResponseStream: HttpResponseStream,
};
}
try {
return await _tryRequire(appRoot, moduleRoot, module);
} catch (e) {
if (e instanceof SyntaxError) {
throw new UserCodeSyntaxError(e);
} else if (e.code !== undefined && e.code === 'MODULE_NOT_FOUND') {
verbose('globalPaths', JSON.stringify(require('module').globalPaths));
throw new ImportModuleError(e);
} else {
throw e;
}
}
}
function _throwIfInvalidHandler(fullHandlerString) {
if (fullHandlerString.includes(RELATIVE_PATH_SUBSTRING)) {
throw new MalformedHandlerName(
`'${fullHandlerString}' is not a valid handler name. Use absolute paths when specifying root directories in handler names.`
);
}
}
function _isHandlerStreaming(handler) {
if (
typeof handler[HANDLER_STREAMING] === 'undefined' ||
handler[HANDLER_STREAMING] === null ||
handler[HANDLER_STREAMING] === false
) {
return false;
}
if (handler[HANDLER_STREAMING] === STREAM_RESPONSE) {
return STREAM_RESPONSE;
} else {
throw new MalformedStreamingHandler(
'Only response streaming is supported.'
);
}
}
function _highWaterMark(handler) {
if (
typeof handler[HANDLER_HIGHWATERMARK] === 'undefined' ||
handler[HANDLER_HIGHWATERMARK] === null ||
handler[HANDLER_HIGHWATERMARK] === false
) {
return undefined;
}
const hwm = parseInt(handler[HANDLER_HIGHWATERMARK]);
return isNaN(hwm) ? undefined : hwm;
}
/**
* Load the user's function with the approot and the handler string.
* @param appRoot {string}
* The path to the application root.
* @param handlerString {string}
* The user-provided handler function in the form 'module.function'.
* @return userFuction {function}
* The user's handler function. This function will be passed the event body,
* the context object, and the callback function.
* @throws In five cases:-
* 1 - if the handler string is incorrectly formatted an error is thrown
* 2 - if the module referenced by the handler cannot be loaded
* 3 - if the function in the handler does not exist in the module
* 4 - if a property with the same name, but isn't a function, exists on the
* module
* 5 - the handler includes illegal character sequences (like relative paths
* for traversing up the filesystem '..')
* Errors for scenarios known by the runtime, will be wrapped by Runtime.* errors.
*/
module.exports.load = async function (appRoot, fullHandlerString) {
_throwIfInvalidHandler(fullHandlerString);
let [moduleRoot, moduleAndHandler] = _moduleRootAndHandler(fullHandlerString);
let [module, handlerPath] = _splitHandlerString(moduleAndHandler);
let userApp = await _loadUserApp(appRoot, moduleRoot, module);
let handlerFunc = _resolveHandler(userApp, handlerPath);
if (!handlerFunc) {
throw new HandlerNotFound(
`${fullHandlerString} is undefined or not exported`
);
}
if (typeof handlerFunc !== 'function') {
throw new HandlerNotFound(`${fullHandlerString} is not a function`);
}
return handlerFunc;
};
module.exports.isHandlerFunction = function (value) {
return typeof value === 'function';
};
module.exports.getHandlerMetadata = function (handlerFunc) {
return {
streaming: _isHandlerStreaming(handlerFunc),
highWaterMark: _highWaterMark(handlerFunc),
};
};
module.exports.STREAM_RESPONSE = STREAM_RESPONSE;