RE: React Portlet: "The provided configuration is not Directed Acyclic Grap

Rich Rein, modified 6 Years ago. New Member Posts: 11 Join Date: 5/31/19 Recent Posts
Some googling has turned me on to the source of a problem that I had run into (essentially, one of the dependencies in my project has a circular dependency, which Webpack is able to handle when I am running in standalone mode - but the Liferay NPM Bundler is not able to handle). In following along with the thread on the github issue (https://github.com/liferay/liferay-amd-loader/issues/182), there is an example of how to use webpack to bundle the offending bits, and then expose that as a dependency for my project - but that example assumes a pretty simple use case (e.g., wrapping a function within oData-parser). In my case, the offending piece (GridOptionsWrapper, deep within the ag-Grid library that I was attempting to use within my React application, to be loaded as a portlet within Liferay) is a bit deeper/harder to expose individually. The same issue thread makes mention of avoiding the bundler and/or using Webpack instead - would this mean somehow using Webpack as a way to wrap the entire library (ag-Grid in my case), exposing that generated bundle as something that I could consume within my project? Or somehow using the overall Webpack-generated bundle itself as the artifact that I somehow load into Liferay (vs. the Liferay NPM Bundler-generated artifact). If so, any best practices/examples, at least at a high level, about how one might start to tackle this?
thumbnail
Ivan Zaera, modified 6 Years ago. Regular Member Posts: 119 Join Date: 10/1/13 Recent Posts
I would say that if you use cyclic dependencies heavily you should switch to webpack entirely (the whole build for the portlet) at least until we find a way to support it.
Our loader follows the AMD spec, and AMD clearly states that cyclic dependencies are not allowed. This is because when you call define() in AMD the dependencies of the module must be already available otherwise it won't work. Node.js can do lazy loading, but the AMD loader cannot.
That said, it looks like we would be able to emulate the behavior of Node.js in an AMD loader by using some kind of lazy proxy for cyclic dependencies, but we haven't yet investigated it. That's why the issue is still open :-(.
There's nothing wrong in using webpack for your build. In fact, we will probably add some examples with webpack alone (no liferay bundler) in this example repo that I have started working with -> https://github.com/izaera/liferay-js-toolkit-showcase/
The advantage of using the bundler is that you can share dependencies between different portlets (see the react example in that repo, for example). With webpack that's impossible because all you get is a "black box" bundle.js that would be loaded by the portlet but can only be consumed by that portlet in particular. So, say you have 5 portlets in a page using React: in that case you would load React 5 times in your JS engine.
Some customers have had success by splitting the webpack bundles and putting some common pieces in themes, but it's a very customized way to do it and seems difficult to generalize :-(.
On the other hand, we will investigate the possibility of using webpack and integrating it with the loader, but we haven't started that yet and don't even know if it would be possible at all.
Rich Rein, modified 6 Years ago. New Member Posts: 11 Join Date: 5/31/19 Recent Posts
Ivan - thanks for the reply! I noted the status of the issue, and sort of understand why the loader doesn't accommodate it - unfortunately, this  is a third party set of components that we had integrated with (and I would rather not try to rewrite their code to remove the cyclic dependencies).What I think that I need is a pointer in the right direction, so that I can make the leap from Webpack-bundled React project (that I can run in a standalone fashion) to a deployable portlet. Is there existing documentation that would be applicable (maybe even from the pre-bundler/loader days?), showing what is needed to wrap an existing project? Or am I missing the point of your answer?
thumbnail
Ivan Zaera, modified 6 Years ago. Regular Member Posts: 119 Join Date: 10/1/13 Recent Posts
Hi Rich:
I'm currently investigating that support (webpack based portlet) using our standard entry point (see https://github.com/liferay/liferay-js-toolkit/wiki/JS-extended-portlets-entry-point but it will probably take some time to see it finished.
However, it doesn't seem too difficult if done by hand. I would say that creating a portlet and adding a footer-portlet-javascript property to it pointing to the webpack bundle would suffice. Then, you need to tweak your build so that you include the webpack bundle in the portlet. Probably calling webpack from gradle in the process resources step would be OK.

Note that sometimes, webpack dumps some JS code to the output HTML file. In that case, you should copy that code into a JSP so that the whole thing starts.
Hope that helps.
Rich Rein, modified 6 Years ago. New Member Posts: 11 Join Date: 5/31/19 Recent Posts
Circling back around, I was able to get this to work. For future reference (especially if it helps save someone else some "head banging against desk" time!), here is a brief summary:

Problem: we wanted to use a third-party component (ag-Grid) which contained some circular dependencies/references deeper within the project - but the Liferay NPM bundler throws the above-mentioned error.

Structure: internal repository (fronting NPM for third-party libraries/packages), React for front-end development, deploying as portlets/widgets to Liferay (currently 7.2)

Solution: I was able to create a basic project (using create-react-library, based upon create-react-app) that simply depends on the package I wanted to use. The index.js of this project simply imports the third-party components and then exports a wrapper component (but could just as easily immediately export the third-party component again), insulating our application developers from the specific component implementation chosen at the time. I then use Webpack to bundle up the dependencies at build time, and then this new wrapper package is published to our internal repository (something like "React UI Grid Components"). The calling application depends on the internal "React UI Grid Components" package (where Webpack has properly handled the circular references, allowing the NPM Bundler to consume something that is able to handle), and deploying to my portal instance goes off without error. The only downside is that now multiple instances of this component on a given portal page will caused duplicate libraries to be loaded - but, in my case I was wrapping a large datagrid component (and have no intention to have more than one existing on a given page).
thumbnail
Ivan Zaera, modified 6 Years ago. Regular Member Posts: 119 Join Date: 10/1/13 Recent Posts
Very interesting technique, Rich. I assume that your webpack module is then consumed through the Liferay loader? Once it is built...
If that was the case there shouldn't be any problem with loading it more than once because that would be handled by the loader itself.
Or am I missing anything?
Rich Rein, modified 6 Years ago. New Member Posts: 11 Join Date: 5/31/19 Recent Posts
That is correct - basically, I am utilizing Webpack to handle working through all of the pieces of the component library that I want to depend on, outputting a Webpack-generated bundle that I can then comsume from my project (with Liferay NPM Bundler being used to build/deploy that consuming application).
Now, the next thing on my life of pieces to work through is how best to manage bundle sizes (I am used to some of the treeshaking features of other bundlers) while still producing Liferay-ready portlets. Currently, I am drawing out lots and lots of boxes on a scratchpad as I work out how all of the various pieces could/should play together...
thumbnail
Ivan Zaera, modified 6 Years ago. Regular Member Posts: 119 Join Date: 10/1/13 Recent Posts
<p>As you probably know by now, we don&#39;t do tree shaking in the bundler, but that&#39;s because we can&#39;t. I mean, we don&#39;t know until runtime who is using what and when, so we need to package everything inside the JARs. However, the loader won&#39;t bother with anything that it&#39;s not requested, so you have there something similar to tree shaking, although only working at the maximum granularity of a JS module (I mean, we cannot remove code from inside a module, just a whole module).</p>
Rich Rein, modified 6 Years ago. New Member Posts: 11 Join Date: 5/31/19 Recent Posts
Sorry, I wanted to play around with a few things before I replied to this - here is what I ran into:
I was developing a widget/portlet which offered a multi-step form. My app initially include the standard React/React-DOM/PropTypes dependencies, some form-specific pieces (Final-Form/React Final-Form, a few custom field type plugins, and then some wrapper-style components I had created to make development of a form easier), and then the actual app itself ( a five step form, with some form fields and conditional logic for each sub-form).

Initially, bundling all of this resulted in a 72MB bundle!

I had already extracted out the standard React dependencies and form-specific pieces into their own package for reuse in other applications, so I next took the step to have this package use peer dependencies for the standard React pieces (such that a given consuming application would provide React/React-DOM/PropTypes), and then used Webpack to build/do a little tree-shaking, resulting in a much smaller and more efficient package.

Now using the Liferay NPM Bundler on the final application resulted in a 57MB bundle - still not entirely ideal, and load times were a bit effected...
For my next step, I created two OSGI bundles - using your example React-provider to bundle React/React-DOM/PropTypes and deploy them to my Liferay instance for importing by any consuming applications (14MB, with no optimization added yet), and adding the Liferay NPM Bundler to my form-specific pieces package (so effectively it gets optimized by Webpack, and then my .npmbundlerrc can exclude all node modules (because Webpack already pulled all of the relevant pieces into the generated JS files) - resulting in a 4MB bundle to also deploy to my Liferay instance.

Lastly, I updated my consuming application in a few ways:
  • Put a Webpack build in front of the Liferay NPM Bundler (to trim down the bundle size of the application itself, as well as tree-shake to get only the dependencies/pieces of dependencies that I actually am using)
  • Exclude: {"*": true}} to not have the Bundler include anything from node modules (because the Webpack build step pulls in what I need)
  • Imports: React/React-DOM/PropTypes from react-provider, form-specific pieces package (both are deployed as OSGI bundles to my Liferay instance)

This took took my application bundle size down to 4MB (plus 4MB for the form-specific pieces package, and 14MB unoptimized for the react-provider) - vs. the initial 72MB when I started. Effectively, I am still getting some efficiency in letting Liferay reuse the react-provider pieces across all of my portlets/widgets, and reuse the form-specific pieces package across any portlets/widgets which include a form (essentially causing me to explicitly define my units of reuse, rather than having the bundler try to simply match across package names - which I understand to be the purpose behind the major changes from the philosophy of bundler 1.x to 2.x anyway) - but my bundle sizes are significantly smaller, and my portlets/widgets load much faster. Can you see any downsides/cons with my approach?