Serverless-webpack: ES6 dynamic import / resolving chunk fails when handler is in subdirectory

Created on 17 Jan 2019  路  11Comments  路  Source: serverless-heaven/serverless-webpack

setup:

  • all source code including code for handlers is in a subdirectory src
  • function will dynamical import a module with import()

problem:

  • chunk files are created but are not found a runtime: Error: Cannot find module './0.js'
  • the chunk files 0.js are always created by web pack in the root directory but referenced relatively to the handler file handler2.js
fails:
    handler: src/handler2.hello
    events:
      - http:
          path: hello-fails/{cmd}
          method: get
          cors: true
.webpack
 - service
   - src
      - handler2.js
   - 0.js

example project
https://github.com/aheissenberger/serverless-webpack-import-problem.git

yarn install
yarn test-ok
yarn test-fails

the example test-ok is a setup with the handler file not moved to a subfolder.

documentation

Most helpful comment

I wrote a simple hack around this limitation that forces the handler to the root of .webpack/service:

First, adjust the entry names in the Webpack config with something like (from './src/path/to/handler' to 'handler'):

const entries = {};

Object.keys(slsw.lib.entries).forEach(e => {
  try {
    entries[e.split('/').splice(-1)[0]] = slsw.lib.entries[e];
  } catch (error) {
    console.info('Assuming handler is within root directory');
  }
});

config = {
  ...config,
  entry: entries,
};

Next, enforce Serverless to look in the same place with this plugin:

.serverless_plugins/sls-hack.js:

'use strict';

class ServerlessPlugin {
  constructor(serverless, options) {
    this.serverless = serverless;
    this.options = options;

    const boundStartHandler = this.startHandler.bind(this);

    this.hooks = {
      'before:offline:start': boundStartHandler,
      'before:offline:start:init': boundStartHandler,
    };
  }

  startHandler() {
    const functions = this.serverless.service.functions;

    Object.keys(functions).forEach(entry => {
      try {
        functions[entry].handler = functions[entry].handler.split('/').splice(-1)[0];
      } catch (error) {
        console.info('Assuming handler is within root directory');
      }
    });
  }
}

module.exports = ServerlessPlugin;

serverless.yml

  - serverless-webpack
  - sls-hack
  - serverless-offline

All 11 comments

I wrote a simple hack around this limitation that forces the handler to the root of .webpack/service:

First, adjust the entry names in the Webpack config with something like (from './src/path/to/handler' to 'handler'):

const entries = {};

Object.keys(slsw.lib.entries).forEach(e => {
  try {
    entries[e.split('/').splice(-1)[0]] = slsw.lib.entries[e];
  } catch (error) {
    console.info('Assuming handler is within root directory');
  }
});

config = {
  ...config,
  entry: entries,
};

Next, enforce Serverless to look in the same place with this plugin:

.serverless_plugins/sls-hack.js:

'use strict';

class ServerlessPlugin {
  constructor(serverless, options) {
    this.serverless = serverless;
    this.options = options;

    const boundStartHandler = this.startHandler.bind(this);

    this.hooks = {
      'before:offline:start': boundStartHandler,
      'before:offline:start:init': boundStartHandler,
    };
  }

  startHandler() {
    const functions = this.serverless.service.functions;

    Object.keys(functions).forEach(entry => {
      try {
        functions[entry].handler = functions[entry].handler.split('/').splice(-1)[0];
      } catch (error) {
        console.info('Assuming handler is within root directory');
      }
    });
  }
}

module.exports = ServerlessPlugin;

serverless.yml

  - serverless-webpack
  - sls-hack
  - serverless-offline

Why not just use CopyWebpackPlugin? I was also running into the same issue and we can make it work with CopyWebpackPlugin

Here's a sample:

plugins: [
  new CopyWebpackPlugin([
    { from: './build/server/service/*', to: './src', test: /.js/, flatten: true },
  ], { copyUnmodified: true }),
],

This will copy all your *.js files in your src folder.

One solution I have found is using magic comments in my dynamic import. https://webpack.js.org/api/module-methods/#magic-comments.
By using /* webpackMode: "eager" */ it forces webpack to not create an extra chunk, but rather keep the module in the current chunk. Sinc everything is in the one chunk you don't have to worry about chunks being in the wrong place like your example.

I don鈥檛 think you should do that since you loose the benefits of lazy loading of that chunk and it鈥檒l get loaded along with your main chunk itself

@HyperBrain is this something we can/should improve upon or should we just add a note to the documentation regarding dynamic imports?

@hassankhan The solution with the CopyWebpackPlugin works and could be mentioned in the documentation for now. The sls-hack solution looks a bit too hacky to me 馃槃

However, I think it might be possible to find a webpack configuration that emits any chunks in the handler's directory, or configure each function's webpack configuration properly while iterating the functions and building the config array. This could be a mid-term solution.

What do you think?

@HyperBrain 馃憤 to adding this to the documentation for the short-term, we should definitely open a new issue to track a longer-term solution though. Closing this issue in favour of adding this to the FAQs in #495.

Not sure why this is closed - I like the idea of adding workarounds until this is fixed, but this also should be fixed. If someone with more knowledge of the codebase can point me in the starting direction, I can try to create a PR :)

@dbartholomae I figured the PR would be a longer-term solution, while users who are experiencing the issue now could refer to the (as yet nonexistent) FAQ. It seemed like there were a few other recurring issues users had and I thought it would be sensible to group them somewhere.

It wasn't my intention to end discussion on the topic, rather I was hoping (and should have probably explicitly said) that someone would create an issue referencing @HyperBrain's comment and any potential pros and cons of the approach he described.

For some reason the CopyWebpackPlugin solution doesn't work for me.
I'm using pdfjs-dist which results in a vendors~pdfjsWorker.js in my webpack output.

And @martinjlowm's solution doesn't work with package.individually.

The problem here as far as I can see is that webpack _does not_ support the way entry configuration is currently being used.

ie. the entry: {[key]: path} object keys are names and not supposed to be paths

I would propose the solution as the webpack entry names should actually just be the function name in serverless.yml config. Then for the handler paths, because Serverless requires the module to exist at the defined path, a simple module facade is created by webpack which simply binds the handler to the proper entry which has been created by webpack.

ie.

# serverless.yml
functions:
  gql:
    handler: graphql/service.http
// webpack.config requirement
slsw.lib.entries = {
  gql: 'graphql/service.ts',
  'graphql/service': 'bind!gql'
}
// .webpack/graphql/gql.ts
// my code bundled, sticking with correct webpack configuration
export function http() {}
// .webpack/graphql/graphql/service.ts
// generated binding file to satisfy serverless given webpack prefers flat bundles with node
export * from '../gql'
Was this page helpful?
0 / 5 - 0 ratings