7

In my project, I want to import specific modules that their actual path is known only during compile.

Let's say that I have components/A.js, components/B.js and components/C.js and in my App.js I want to include a subset of them that will be known only in webpack's compile-time (ex. from a JSON)

What I 've tried - 1

//App.js
compileData = await getCompileDataSomehow()

import("./components/" + compileData.component + ".js")
   .then( /* Do stuff with component */);

This work's nice, but webpack will go and create a chunk for every components/*.js file. Also, there will be extra network roundtrips. I feel this is a bit overkill since I will know what component I want when webpack will run. So I tried to Provide the array of components without luck because require([...]) is also considered dynamic.

What I 've ended up with

//App.js
import("./components/ALL.js") //this file doesn't actually exists
//webpack.config.js

const fs = require('fs')

const componentsToInclude = ["A", "B"] //this will be read from a json


fs.writeFileSync('./src/components/ALL.js',
  `
export default [
    ${componentsToInclude.map(comp => `require("./${comp}.js")` )}
].map(c => c.default);

`)

module.exports = { // config //}

This works! But as you can see, this is not an elegant solution, and bugs can easily happen. So does anyone know a proper way to deal with this situation?

Alex Michailidis
  • 4,078
  • 1
  • 16
  • 35

1 Answers1

1

In this answer I'll assume that, similar to the question, there's a main App.js file, and a directory called components containing files A.js, B.js, C.js, D.js, etc. Only A.js, B.js need to be bundled, and all other files should be excluded from the bundle, but this is only known at compile time.

require.context

Webpack provides a way to specify a group of dependencies with the custom, Webpack-specific require.context method. require.context takes four arguments: a path to a directory with the files to include, a boolean to determine whether subdirectories are included, a regular expression to filter files, and a directive for the loading mode.

To bundle all files in the components directory we'd run

// App.js
const req = require.context('./components')

(This is beyond the scope of the original question, but you'd then use req to run any exported functions from those files. For example, if each .js file had a default function exported, to run each default function you'd do:

const req = require.context('./components')
req.keys().forEach( key => {
    req( key )?.default()
} )

For more information in requiring with require.context see this question and answers)


Since we want to load only A.js and B.js we must send a regular expression to require.context to filter the files. Let's suppose that we knew we'd only need A.js and B.js when writing App.js. We could filter the files with a hardcoded regular expression like so:

// App.js
const req = require.context('./components', true, /(A|B)\.js$/)

/(A|B)\.js$/ is a simple regular expression that returns true if the input is A.js or B.js. It could easily be extended to allow more files: /(A|B|C|D)\.js$/, or to only search specific subdirectories. Webpack will evaluate the regular expression at compile time and only include matching files in the bundle.

However, the problem remains: how do we determine what the filter is when we only know the filenames at compile time?

webpack.DefinePlugin

Using webpack.DefinePlugin we can define global variables in our webpack.config.js that are defined at compile time, and that Webpack makes statically available to every file. For example, we could put our regex from before into a global variable called __IMPORT_REGEX__:

//webpack.config.js
module.exports = {
    ...
    plugins: [
        new webpack.DefinePlugin( {
            __IMPORT_REGEX__: "/(A|B)\\.js$/"
        } )
    ]
}
// App.js
const req = require.context('./components', true, __IMPORT_REGEX__)

Note that the value of variables created via DefinePlugin must be stringified; that is, we can't supply a raw RegExp value. This means that any backslashes must be escaped.

If we have the names of our files in an array, perhaps previously read from a JSON file, we can dynamically build the needed regular expression in our webpack.config.js. In this example it's a simple case of joining the array with the | character as a separator:

// webpack.config.js
const fileNames = ["A", "B"];

const importRegex = `/(${ fileNames.join("|") })\\.js$/`; // will return '/(A|B)\\.js$/'

module.exports = { ...

Putting it all together

// webpack.config.js
const fileNames = ["A", "B"];

const importRegex = `/(${ fileNames.join("|") })\\.js$/`;

module.exports = {
    ...
    plugins: [
        new webpack.DefinePlugin( {
            __IMPORT_REGEX__: importRegex
        } )
    ]
}
// App.js
const req = require.context('./components', true, __IMPORT_REGEX__)

One final note: Typescript users will need to declare a global for __IMPORT_REGEX__

// interface.d.ts
declare global {
    var __IMPORT_REGEX__: RegExp
}
jla
  • 4,191
  • 3
  • 27
  • 44