AnomalyInnovations/serverless-bundle

Dynamically importing @aws-sdk client modules

henhal opened this issue · 1 comments

My application has a dependency to a library module, which in turn has @aws-sdk/client-s3 as a peerDependency, and uses a dynamic import to import it, since that functionality is optional.
My application then has a dependency to @aws-sdk/client-s3.

When bundling with serverless-bundle without any extra config steps, unsurprisingly @aws-sdk/client-s3 is not bundled due to not being statically referenced anywhere, so I add the following:

bundle:
  forceInclude:
    - "@aws-sdk/client-s3"

This creates a zip containing

src
  my-function
    index.js
node_modules
  @aws-sdk
    client-s3
    < a bunch of sub-dependencies of client-s3 >

So far so good, but the library is still not able to load @aws-sdk/client-s3 in runtime:

const {S3} = await import('@aws-sdk/client-s3');
const s3 = new S3({region: 'eu-west-1'});

This produces an error like this:

error:  webpack:/// < ... > :2\n\tvar e = new Error(\"Cannot find module '\" + req + \"'\");\n\t        ^\n\nError: Cannot find module '@aws-sdk/client-s3'\n 

I'm not a very experienced webpack user but I love using serverless-bundle with it's almost-zero config, but when encountering this I realize I don't understand how webpack performs module resolution.
Is there something else I need to add? The README specifically has an example of how to deal with a dynamically imported module using forceInclude. So how come the module is included in node_modules but not resolved?

To be clear: dynamically importing the @aws-sdk/client-s3 lib from my app works when forceInclude is present, but dynamically importing it from the lib does not. So to me it seems to be about the path to node_modules that the lib uses. While all the code in my-lib is bundled into the single index.js file, calls to await import('@aws-sdk/client-s3') only works from code that was originally in my-app, and not in my-lib.

For clarification, the setup is something like this:

my-lib:

package.json:

  "devDependencies": {
    "@aws-sdk/client-s3": "^3.83.0"
  },
  "peerDependencies": {
    "@aws-sdk/client-s3": "^3.83.0"
  }

index.ts

export async function testS3fromLib() {
  const {S3} = await import('@aws-sdk/client-s3');
  return new S3({region: 'eu-west-1'});
}

my-app:
package.json:

  "dependencies": {
    "my-lib": "1.0.0",
    "@aws-sdk/client-s3": "^3.83.0"
  }

index.ts:

import {testS3fromLib} from 'my-lib';
testS3fromLib().then(console.log, console.log); // prints 'Cannot find module' error from code in my-lib

async function testS3FromApp() {
  const {S3} = await import('@aws-sdk/client-s3');
  return new S3({region: 'eu-west-1'});
}

testS3FromApp().then(console.log, console.log); // works, prints s3 object instance


I had quite some trouble fixing this, but I ended up with not having to use forceInclude if my dynamic imports were distinct enough.

At first I had code that imported a few different AWS SDK clients semi-dynamically through a utility function with something like await import('@aws-sdk/' + clientName) but this caused webpack to package all files matching @aws-sdk, and I mean all files; I got lots of errors about webpack not having any loader for .md files, .d.ts files etc. This had be stumped for a full day before realizing that if I make my dynamic imports "static enough" by not building the import path in runtime at all, it would include only the modules I need (if they exist) and as a side-effect not include non-TS/JS files such as .md etc.
I don't understand why having a wildcard import like '@aws/sdk' + myClient makes webpack try to package README.md files etc, but at least I got it working by narrowing my dynamic imports down.
But perhaps there's a new default config to add to serverless-bundle to make webpack only include *.js and *.ts (but exclude .d.ts)?

Anyway, I got this working with peerDependencies and a dynamic import if the dynamic import has a string literal for the full import path, i.e., known in compile time.