13

I have a project structure that contains a folder that I want to dynamically import into a UMD module when I build my webpack config and have each file be a submodule in the outputted build.

For example, let's say my sources look like:

/src/snippets/red.js
/src/snippets/green.js
/src/snippets/blue.js

I want webpack to build these sources into a single module that allows me to access each one as submodules, like so;

const red = require('snippets').red

I tried iterating over the snippets directory and creating an object with filenames and paths, but I can't figure out what configuration property will instruct webpack to bundle them into a single file and export each. Here's what I have so far:

const glob = require('glob')

module.exports = {
    entry: glob.sync('./src/snippets/*.js').reduce((files, file) => {
        files[path.basename(file,'.js')] = file

        return files
    }, {}),
    output: {
      path: __dirname + '/lib',
      filename: 'snippets.js',
      libraryTarget: 'umd'
    }
}

This errors out with: Conflict: Multiple assets emit to the same filename ./lib/snippets.js

Any idea on how I can accomplish what I'm looking for?

Andy Baird
  • 6,088
  • 4
  • 43
  • 63

3 Answers3

3

Been using a modified version of this solution in production for a while now, and I have not had any issues with it. (My company's set up has a slightly more elaborate filter, to exclude certain mock modules, but is otherwise the same).

We use a build script to crawl the directory in question (in our setup, it's src but in yours its src/snippets. For each file that ends in .js, we import it and re-export it in src/index.js. If you need something more robust, like going multiple levels deep, you'll need to modify this to recursively traverse the directory structure.

When this is done, we output it to index.js, along with a reminder that the file is auto-generated to dissuade people from manually adding entries to the file.

const fs = require("fs");
const { EOL } = require("os");
const path = require("path");

let modules = 0;
const buffer = [
  "// auto-generated file", "",
];

const emitModule = file => {
  const moduleName = file.replace(".js", "");
  modules += 1;
  buffer.push(`exports.${moduleName} = require("./snippets/${moduleName}");`);
};

const files = fs.readdirSync(__dirname + "/snippets");

files
.filter(fname => fname !== "index.js" && !fname.startsWith("."))
.forEach(f => {
  const stats = fs.statSync(path.join(__dirname, "snippets", f));
  if (stats.isFile()) {
    emitModule(f);
  }
});

fs.writeFileSync(path.join(__dirname, "index.js"), buffer.join(EOL)+EOL);

console.info(`Built 'src/index.js' with ${modules} modules`);

Then, in webpack.config.js, we set the libraryTarget to be umd as follows:

module.exports = {
  entry: path.resolve(__dirname, "src/index.js"),
  output: {
    path: path.resolve(__dirname, "build/"),
    filename: "mylib.js",
    libraryTarget: "umd"
  }
};

Finally, for convenience, in package.json, we use the following to automatically run the build script before building (you can also use it before starting webpack-dev-server, or running mocha tests).

This set up feels rather hacky, but it works pretty well. The only issue I've ever come up against is that occasionally, the order of the modules will be changed around (presumably, because of environment differences), and the enumeration of the files causes a false positive in git.

I've put the whole package up on GitHub here: https://github.com/akatechis/webpack-lib-poc


Update: If you don't want to manually call the build script by adding it to your package.json scripts, you can always wrap it as a webpack plugin. You can read details here

In short, a plugin is just an object with an apply method that registers itself with the webpack compiler. From the docs:

As a clever JavaScript developer you may remember the Function.prototype.apply method. Because of this method you can pass any function as plugin (this will point to the compiler). You can use this style to inline custom plugins in your configuration.

Here are the changes between the two setups:

Change webpack config to import your build script and add it to the plugins array:

const path = require("path");
const buildPlugin = require("./src/index.build");

module.exports = {
  entry: path.resolve(__dirname, "src/index.js"),
  output: {
    path: path.resolve(__dirname, "build/"),
    filename: "mylib.js",
    libraryTarget: "umd"
  },
  plugins: [buildPlugin]
};

Then change index.build.js to export a function that registers a callback on the compiler (it receives it as this). Take the contents of the build script from before, put it into a build() function, and then export a plugin function as follows:

module.exports = function () {
  this.plugin('run', function(compiler, callback) {
    console.log("Build script starting");
    build();
    callback();
  });
};

Remove the build-index target from any scripts in package.json. Webpack will now always call your build script before running.

  • This does exactly what I want in the end, but is there any way to get webpack to execute the additional build step? – Andy Baird Jan 29 '18 at 03:13
  • Absolutely, you can wrap the build script as a webpack plugin, and drop it into the `plugins` config option. I'll update my answer with a bit more information on this. – Alexandros Katechis Jan 30 '18 at 18:41
0

Try this:

I am basically changing the entry slight differently and then further using NamedModulesPlugin.

module.exports = {
    entry: glob.sync('./snippets/*.js').reduce(function(entry, file){
        entry['./snippets'].push(file);
        return entry;
    }, { './snippets': []}),
    output: {
        path: path.resolve(__dirname, ''),
        filename: '[name].js'
        libraryTarget: 'umd'
    },
    module: {
        // only relevant portions shown
        plugins: [
            new webpack.NamedModulesPlugin()
        ]
    }
};

it should work.

bhantol
  • 9,368
  • 7
  • 44
  • 81
  • This seemed to compile to a module that just returns the last file in the snippets directory as it's export. It doesn't export all the files as submodules. – Andy Baird Jan 26 '18 at 18:16
  • What version of webpack are you using ? – bhantol Jan 26 '18 at 18:20
  • Webpack Version 3.8.1 gist of my config: https://gist.github.com/ajbdev/4b3ecef76169bd6576c878d4a1d7b7e3 – Andy Baird Jan 26 '18 at 19:11
  • We need to create export barrels ourselves. For `const red = require('./snippets').red` to work in the same project you will need `src/snippets/index.js` which creates export barrels for re-exporting from each file. e.g. `export * from './red';export * from './blue'`; – bhantol Jan 28 '18 at 06:13
  • https://github.com/yogeshgadge/webpack-folder-barrel.git sample of what I am talking about. – bhantol Jan 28 '18 at 06:25
-1

Look similar to conflict-multiple-assets-emit-to-the-same-filename and this solved issue, so it may probably help you. I see somewhere that it also may be due to one (or more) of your module plugin which not support packing.

A. STEFANI
  • 6,707
  • 1
  • 23
  • 48