I'm struggling to dynamically load modules when running a NodeJS service on Lambda after bundling the service with webpack. This runs fine locally when I run transpiled TypeScript directly out of my src/ directory.
The use case is as follows,
- An EventBridge Scheduler fires and calls a Lambda function with a plugin definition defined in the details object.
- The Lambda function uses the plugin definition to look up the module defined by that plugin.
- I dynamically load the plugin module, and execute the code.
The code snippet that performs this import is simple:
const hookPath = path.join(process.cwd(), plugin_data[campaign].hook);
const hookModule = await import(hookPath);
const hookFunction = hookModule.default;
if (typeof hookFunction === 'function') {
// dispatch the event!
logger.info('cron handler is dispatching new event for '+campaign);
await hookFunction(campaign,plugin_data[campaign].cron_config);
} else {
logger.error('hookModule is not a function');
throw new Error('bad hook reference');
}
In all cases, the plugin hooks are defined in a directory called "plugins/". The hook definition referenced in the code snippet is a file path that looks like this - "plugins/default_plugin.js"
All of the plugin parsing is working as intended. It only throws an error on the import statement. This loads and runs correctly locally as well.
My webpack configuration is the following.
const path = require('path');
const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = [
'cron_lambda',
// 'sms_lambda',
// 'api_lambda',
].map((lambdaName) => ({
entry: `./src/${lambdaName}/${lambdaName}_handler.ts`,
target: 'node',
mode: 'production',
output: {
path: path.resolve(__dirname, `dist/${lambdaName}`),
filename: `${lambdaName}_handler.js`,
libraryTarget: 'commonjs2', // Required for AWS Lambda
},
resolve: {
extensions: ['.ts', '.js'],
plugins: [new TsconfigPathsPlugin()],
},
plugins: [
new CopyWebpackPlugin({
patterns: [
{ from: 'src/plugins.json', to: '.' },
{ from: `src/${lambdaName}/plugins/*.js`,
to: ({absoluteFilename}) => `plugins/${path.basename(absoluteFilename)}`,
noErrorOnMissing: true
},
{ from: 'src/utils/*.js',
to: ({absoluteFilename}) => `utils/${path.basename(absoluteFilename)}`,
noErrorOnMissing: true
}
],
}),
],
module: {
rules: [
{
test: /\.ts$/,
loader: 'ts-loader',
exclude: /node_modules/,
options: {
// Ensure all files, including dynamic imports, are transpiled
transpileOnly: false,
},
},
],
},
externals: [
// Exclude AWS SDK since it’s available in the Lambda runtime
'aws-sdk',
({ request }, callback) => {
if( (request && request.startsWith('./plugins/')) ) {
return callback(null, `commonjs ${request}`);
}
callback();
}
]
}));
Some things I know with certainty:
- My zip file that is deployed to Lambda has the plugin module and it is listed in the correct folder.
- I have run fs.existsSync() on the plugin file inside of Lambda and verified that it exists at runtime.
- I've tried constructing the file name multiple ways in addition to the snippet shared above. This does not appear to be a file path problem.
- The code executes correctly when run locally.
How is my webpack configuration impacting the behavior once my distribution folder is put together?
dist/
plugins/
utils/
plugins.json
cron_lambda_handler.js