I’m submitting a feature request
Webpack version:
2.1.0@beta.15
Please tell us about your environment:
Windows 10
Current behavior:
When using System.import (I am using webpack 2), webpack uses its context system (https://webpack.github.io/docs/context.html) to determine the path. In my case it is impossible for me to determine the URL before runtime, as it is attached to a Window
object by the server when it starts up. Before I switched to webpack, I was using SystemJs to import these files, and since every file was transpiled from typescript and was sitting on the disk, the system.import was fine because it would just navigate to that file on the disk. With webpack, however, it needs to know about these files before runtime so it knows what to bundle and how to set up the linking and whatever else it does.
The context system exist to somewhat circumnavigate this issue, but I find it very lacking. The way it is set up is it determines what that import statement could possibly be, and imports all of them preemptively. This was an issue at first since the import statement was System.import(path)
, so I switched that statement to provide more context for the context engine. However, I am trying to import 60-100 files this way (they are 5-10 line angular components), and the highest directory they all share is app/
itself. They are scattered throughout the entire app, as these are all companion components that provide documentation for all of the main components. This breaks down the context system, because aside from importing absolutely everything preemptively, it has no other options.
I restructured some of the app to collect these files in 4-5 folders throughout the app. Even with this, the only option webpack’s context system gives is to have a System.import statement written with the appropriate context for each folder. For example:
System.import('path/to/folder/a/' + filename + '.ts').then();
System.import('totally/different/path/to/folder/b' + filename + '.ts').then();
This is a big issue because now what should be a single import statement is now 2 in the source, which would have to be wrapped in if/else statements and if anyone looked at this code would immediately think this code is extraneous and inefficient. And the other issue is I just had to change my source code to support a specific bundler which would have to be documented, and in 3 months when the next cool whatever.js comes out and we want to switch, we have code in our source that is specifically for webpack, which is not desired.
I understand this is a very complex issue, because I am asking webpack to bundle files that will not be determined until runtime, i.e. once webpack has finished bundling. However these System.import() are fully supported by the ES6 spec*, and so they will not be going away anytime soon, and especially in my codebase I expect to start seeing them more often with even more complicated/obscure paths.
*well maybe not fully I do not know the specifics of the spec (and don’t exactly care to know) but they aren’t going away.
Feature Request:
I would like to let webpack know about these files via a config, so that I do not have to alter my source code. If I could setup contexting via a plugin, so that webpack would know what to bundle, it could generate its map thing or whatever it does, and then associate that set of files with a specific System.import() (or maybe all of them with some option) then it could handle fairly complex pathing. Not only could you specify the exact file path, but you could also give the plugin the same context you give System.import() now and it would do what it already does and would grab all possible dependencies. Since I have these files scattered throughout my app in various folders, this kind of specificity is needed.
- Browser: [all]
- Language: [TypeScript]
You are in luck, as there is already a plugin doing exactly this: The ContextReplacementPlugin.
It allows to configure a regexp like this:
Or it allows to set the exact mapping (webpack 2):
Awesome. This is really relevant question for Angular2 users with the dynamic AppModule resolver. Will need to add this to documentation for sure.
Also @mover96, thank you for the thoughtful and detailed question. This really helps us solve/answer things. You deserve some🎉 🎉 .
I’m going to summarize my findings in case anyone runs into this thread, and I will also explain why the current system, even with
ContextReplacementPlugin
is inadequate.The Goal:
First let me better explain what exactly I am trying to accomplish. I have a 600 module core app, most of which are Angular 2 components. This app already takes around 90 seconds to build (even with the vendors cached with the dll plugin) so it is a medium sized app. The vast majority of components are in a folder of their own name, along with a styleguide folder. The styleguide folders contain 3 files, a sample component, and an html and scss file for the component.
On the server side (.NET Core) I search the entire file tree for these styleguide folders, and put all the sample component names and paths into one big object. There are about 150 of these components (these are not including in the original 600). That object gets attached to
window
and that is the end of the server side stuff. Before I incorporated webpack into the build, I used SystemJs to grab these component’s paths off of the window and dynamically load them using Angular’sDyanmicComponentLoader
like so:Snippet 1
The dynamic loading and displaying of these components is handled by an entirely separate Angular app. And since the files were on the disk and already transpiled in the same directory structure this worked perfectly.
How the
ContextReplacementPlugin
Works:Now, here is how to use the
ContextReplacementPlugin
to accomplish the same goal with minimal source file changes (see the initial post as to why I do not want to change anything in my source, which still wasn’t even accomplished with the plugin). Keep in mind that these folders are literally down just about every tree in the main app, and this is the only solution I discovered. My import statement now looks like the following:Snippet 2
and my
ContextReplacementPlugin
is setup as follows:Snippet 3
The
ContextReplacementPlugin
will override the contexting for anything that matches the regex of the first parameter, theresourceRegExp
(see Note 1). This is useful as in some situations, for catching all instances of your app trying to import a specific dependency. I think this is actually the main purpose of this plugin (see Note 2). However, for my situation (and I am sure there are plenty of other situation where this is needed) I needed to be at the root of my app with the first section of theSystem.import
parameter string (since that is the highest directory that all the sample files share). Here is the first major issue:Issue 1:
The plugin only uses the first portion of the string in the
System.import
(i.e. before the first concatenation with a variable) to get the context.So, if I leave the code how I had it in Snippet 1 (see Note 3), the app would try and get its context from the main app directory. Which isn’t bad, until you add the plugin to the mix. Since the plugin matches off of the first string in the
System.import
statement, my selector MUST be/\.\.\/\.\.\//
(or basically../../
but with regex) which causes theContextReplacementPlugin
to recontext any import statement (see Note 4) that has../../
in it, thus intercepting requests not from just this specificSystem.import
, and causing a slew of missing modules.So to circumvent this, I use the
resourceRegExp
as a unique identifier, so I am 100%% certain that the plugin is only affecting thisSystem.import
. Thisgroup1/
“directory” doesn’t even exist in my file structure, and that is fine because anything that gets context’d is changed to simply./
.The next thing the plugin does is goes to where the second parameter,
newContentResource
points. This is relative to where theSystem.import
statement is. This then goes through and, the if third parameter,newContentRecursive
is set to true, recursively goes down the file structure looking for any files or folder that match the regex in the 4th parameter,newContextRegExp
. Luckily for me the files I am looking for all start withsample_
so I am able to trace my entire file tree and registering all files withsample_
in their name with webpack. While this works, it significantly increased by build times (from 90 seconds to 450 seconds) and comes with a few issues itself.The Remaining Issues:
Issue 2:
The path the gets registered with the plugin (i.e. whatever file path gets found with the
newContentResource
andnewContextRegExp
) must be the same exact string that yourSystem.import
ends up calling. So if theContextReplacementPlugin
finds../../path/to/some/file/sample_01.ts
, yourSystem.import
must call the exact same string since it is mapped to the module when it is bundled. This might not be a big deal if the other issues get taken care of, but this prevented me from using some pretty hacky alternatives to get this up and running.Issue 3:
There is no way to “chain” multiple
ContextReplacementPlugin
s together. This kind of goes back to the initial post, where my solution was to give enough context to theSystem.import
to hit a certain directory, but I did not want to have to write 5 statements. It would be nice to be able to have thenewContentResource
be different with multiple instances of the plugin, so that I can help narrow down the search to just a few directories (and then I still don’t have to add a file to a list everytime I add a new component). But if this was implemented then it would have issues with Issue 2 still since all the paths would have to start from the same place, which in my app is forced to beapp/
.Issue 4:
This is the straw that broke the camel’s back from my project. I have 2 of these
System.import
statements that are identical. But one is for opening the component in an isolated view. The point is by having the same statement, even with the same “unique identifier”, webpack still had to search the entire app structure again looking for matches for thenewContextRegExp
. This had the same increase on my build time yet again, going from 90 seconds originally to around 800 seconds. Webpack can handle 600 dependencies at 90 seconds, but by loading 200~ with this discovery method causes the build time to skyrocket.Summary:
I hope that this provides some more information on how to use the
ContextReplacementPlugin
to its fullest extent. I hope that this also shows some of the more fundamental issues with using the plugin in its current state to really be able to have webpack be compatible with these dynamic imports (which is a very difficult issue for a static bundler!). Here is a brief summary of the core functionality a revamped/new plugins should aim to provide:System.import
. Maybe even likerequire.context
(see Note 5)System.import
that was registered with the plugin’s map.System.import
s are tagged with the same unique identifier (or however you target the statement, i.e. number 1 above), that webpack can just associate both statements with the same context instead of having to search the tree again.I hope this was informative, please let me know if there are any mistakes or if any part is difficult to understand. I still have a hard time with some of the Javascript fundamentals, so it is possible that I misinterpreted the source. As you might be able to tell with this post I love documentation, so let me know if this would be beneficial to add elsewhere as well.
Notes:
/.*group1\//
working even though it should match. Same with beginning and ending line queries, avoid using^
and$
.Critical dependencies
warning) it would acutally be structered as follows:System.import('../../' + this.componentDetails.path + '/styleguide/' + sampleFileName).then
(sorry I had to do this inline or MD restarts my numbering lol)System.import
that works fine with the default contexting engine and it was getting hijacked by the plugin and causing errors.require.context
to set up contexting my way, and then passing the path to thereq
object it returns. This worked great, but then I cannot use theSystem.import
which I want to use for the chunking and dynamic loading. So I though trying something like this would work:System.import(req(path))
. It does not for obvious reasons, since you cannot import an already imported module.I’ll ping you in the very near future. Thank you so so so so so so so so so so so so much.
Thanks @mover96 for writing this up. I think the resolver / context system is by far one of the most interesting features of Webpack and I have also been struggling with the exact same things you described.
The solution I have wandered towards involved developing my own registry of file paths, defining regex style routes, using
path-to-regexp
to turn each of these paths into objects that contained metadata and other details which could be inferred by their file name / folder name and by parsing the file / analyzing the ASTe.g.
This allowed me to build a database of tons of React components each which have accompanying files such as stylesheets, tests, documentation, examples. So I could do something like:
or query all components by certain patterns to find out information about them
Using these types of queries, my desired goal was to be able to use webpack to be able to bundle them up for certain purposes — whether it was to use it in production or to generate a kitchen sink gallery type page, or to find all of the existing components which lacked documentation or tests and auto-generate these files with useful content
The constraints you describe above have made my attempts less than stellar.
I ended up using Webpack’s resolver plugins to take a require or import statement, used the request data Webpack provides, and used that to infer the intended module / file that the user was requesting and handled the resolution on my own using the metadata I gathered. I then passed webpack the absolute path and whatever loader information was required.
I still have a lingering suspicion that my solution could be much much cleaner by using a context replacement plugin but after reading your write up I am not sure. In either case I would love to take another crack at it and use more of webpack’s internals to do it rather than rely on my own hacky and slow version.
Regardless, @TheLarkInn I would love to tag along if you ever do a walk through of Webpack’s code base because I feel after this battle I could clean up a little and jump right into contributing.
I’d basically trade doing a bunch of chores on Webpack for some enlightenment.
@mover96 I’d love to see your project as well because it sounds very interesting
BTW:
An interesting project to check out is the https://github.com/yuanyan/haste-resolver-webpack-plugin
This provides an experience similar to
react-native
using facebooks internal@providesModule
system which works by parsing the docblock . This allows you to move a file anywhere you like and still be able to require it without having to update all of the dependencies which use relative requires, which I gather is similar to what you are after.