Reputation: 187
Here goes a long one. Even though I don't expect much (or anything) in terms of answers or solutions, still feels like a fun interesting problem to share/vent.
I have an app with 2 entry points. They both import the same file, main.ts
which in turn imports a Handlebars template main.hbs
:
entry1.ts
└ main.ts
├ main.hbs
└ …
entry2.ts
└ main.ts
├ main.hbs
└ …
main.ts
also imports other TS classes (thousands) which import other HBS templates (hundreds), but almost all of that is shared between the two entries. The entries simply call the Main class from main.ts
with some entry-specific options.
I was asked to create an entry-specific "variant" to the HBS templates so that entry1
would load main.v1.hbs
and entry2
would load main.v2.hbs
, if such files exist. And if any other imported .ts
files import any other .hbs
files, they would also return the respective *.v1.hbs
/*.v2.hbs
variants.
Since I wanted it to be as much automated as possible and have as little changes to the actual source code as possible, I decided that the way to go was to have Webpack "redirect" the imports:
entry1.ts
imports main.ts
imports main.hbs
which actually loads main.v1.hbs
main.ts
imports menu.ts
which actually loads menu.v1.hbs
entry2.ts
imports main.ts
imports main.hbs
which actually loads main.v2.hbs
main.ts
imports menu.ts
which actually loads menu.v2.hbs
I figured this approach would yield me multiple advantages:
Don't have to change the source code in the requiring files (literally hundreds of them).
If I didn't care about this this, I could've used a require with expression and then used tree shaking to leave only the templates that are used in each entry. However…
require()
syntax and not ES-style static import
-- so not forward-compatibleCould check whether a variant file even exists before "redirecting", and fallback to the default file if it doesn't.
Importantly, this allows me to do this transition incrementally, as more files of the new "v2" variant are created, without keeping a static list of which imports map to which files or whatever.
Maybe possible to do this transformation within a single compilation run.
If I didn't care about this, I could just compile each entry separately, setting the appropriate options on each run. However the application is quite large, takes several minutes to build and even requires increasing the Node memory limit. Because of that, building each entry separately in sequence or in parallel just isn't too great, though it is my last resort if nothing else works. Anyway I feel like this is something that I should be able to do within a single build, given Webpack's capabilities.
This is a plugin built-in to Webpack which at first glance seems to do what I need: intercepts require
calls for specific modules and changes them to other modules, and it even has regex and predicate function support. However I quickly abandoned this due to the fact that the mapping cannot change during compilation. This meant I couldn't have a different replacement rule for each entry.
Now we're deep in the trenches. I figured, why not write my own loader to solve this? A loader is capable of reading its root entry so in theory I should be able to use that information to, say, instead of main.hbs
load main.v1.hbs
for entry1
and main.v2.hbs
for entry2
.
While initially this seemed to work (even though I hated the non-stateless-ness of this apporach), I discovered that Webpack seems to cache the require/resolution th1e first time it does it: while processing main.ts
. So even with all the logic I had, my loader was called only once per file, not once per require
and I wasn't able to achieve what I wanted.
I investigated ways to tell Webpack "don't cache this, read it again the next time", I had no luck. Since both entries import main.ts
which imports main.hbs
just once, Webpack treats both the TS and the HBS as just 1 module each, regardless of the fact that they are imported into multiple entry files. I guess it's a must-have optimization but I didn't find my way out of it, for this specific case.
Since a Loader didn't get me what I needed, I tried writing a plugin. I went through the detailed documentation and tried hooking into the compiler
and compilation
but didn't get too far. I looked through NormalModuleReplacementPlugin
's source and hooked into NormalModuleFactory
the same way (the documentation doesn't seem to cover it). Eventually had more-or-less re-implemented the functionality of my previous Loader attempt within the Plugin system by changing the requests of the appropriate resources to include a per-entry "variant" -- just what I needed. Sadly, however, I hit the same roadblock as with the Loader -- the file (and my code) gets accessed just once.
I also tried going "from the outside in" -- looking at the resulting chunks where each entry had modules of the HBS files within its tree, but those were already processed and compiled and that didn't seem like a road to success.
Finally, in a similiar vein to a regular Plugin, I decided to try with a resolve Plugin. All the time I was thinking, "I just need these require
calls to resolve to someplace else, that shouldn't be hard!"
But that didn't work aswell, had the same "hooks called only once per file" problems and now I couldn't even get the entry point from the required file, since they weren't even properly resolved yet.
So if I could get my Plugin or Loader to tell Webpack "hey, this file will be different the next time I require
it so please check it again", that I think would solve everything and make everything work the way I wanted it to.
If that's not possible, then I will probably just revert to sequential builds:
entry1.js
with *.hbs
resolving to *.v1.hbs
entry2.js
with *.hbs
resolving to *.v2.hbs
and burn time and RAM. But what can you do.
Thank you for reading.
Upvotes: 6
Views: 1005
Reputation: 106
I have the same problem. And the only think helped me is Webpack Virtual Modules. I dynamically generated several instances of entry modules depending on some template. So these modules had different IDs and didn't share between entries.
Upvotes: -1