As of LPD-48372, the amd-loader has officially been deprecated in Liferay DXP. That means it’s no longer enabled by default—and with that, so ends the era of liferay-npm-bundler and amd-loader.
But don’t worry. Taking advantage of Browser modules and
migrating to standard JavaScript tooling like esbuild, webpack, or vite is not only doable, it’s
refreshingly simple. In this post, I’ll walk you through exactly how
to modernize an existing Liferay project using one of these tools (in
our case, we’ll use esbuild).
Let’s jump in!
The Starting Point
For this guide, we'll assume you're working with a project
created using the npm-react-portlet Blade template.
This template was originally set up to
use liferay-npm-bundler, but we’ll swap that out for
something leaner and more modern.
Clean Up the Old Setup
First things first, let’s remove files we no longer need:
-
.babelrc -
.npmbundlerrc
These were used by Babel and
the liferay-npm-bundler, but since we're switching
tools, they’re no longer needed.
Update Your package.json
Here’s what the old package.json might look like:
{
"dependencies": {
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"liferay-npm-bundler": "2.30.0"
},
"main": "js/index.js",
"name": "react-portlet",
"scripts": {
"build": "babel --source-maps -d build/resources/main/META-INF/resources src/main/resources/META-INF/resources && liferay-npm-bundler"
},
"version": "1.0.0"
}
Now, let’s simplify things with esbuild:
{
"dependencies": {
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"esbuild": "^0.20.2"
},
"main": "js/index.js",
"name": "react-portlet",
"scripts": {
"build": "esbuild ./src/main/resources/META-INF/resources/js/index.js --bundle --outfile=./build/resources/main/META-INF/resources/js/index.js --loader:.js=jsx --format=esm --external:react --external:react-dom --external:react-dom/client"
},
"version": "1.0.0"
}
What changed?
- Removed Babel
and
liferay-npm-bundler(goodbye clutter 👋) - Added
esbuildto bundle code (you can use the tool of your choice)- input:
./src/main/resources/META-INF/resources/js/index.js - output:
./build/resources/main/META-INF/resources/js/index.js
- input:
- Updated the
buildscript to bundle your code using ESM format- Additionally
declared
reactandreact-domas externals to use Liferay DXP’s provided versions.
- Additionally
declared
Update the Java Class (ReactPortlet.java)
Here’s a look at the old implementation:
@Override
public void doView(RenderRequest renderRequest, RenderResponse renderResponse)
throws IOException, PortletException {
renderRequest.setAttribute(
"mainRequire",
_npmResolver.resolveModuleName("react-portlet") + " as main");
super.doView(renderRequest, renderResponse);
}
@Reference
private NPMResolver _npmResolver;
We can now simplify it drastically:
public class ReactPortlet extends MVCPortlet {}
What changed?
- Removed the need for
NPMResolver - No longer setting
mainRequiremanually
Update Your View (view.jsp)
Here’s what the old view file looked like:
<%@ include file="/init.jsp" %>
<div id="<portlet:namespace />-root"></div>
<aui:script require="<%= mainRequire %>">
main.default('<portlet:namespace />-root');
</aui:script>
And here’s the modern version using native ES Modules:
<%@ include file="/init.jsp" %>
<div id="<portlet:namespace />-root"></div>
<aui:script type="module">
import App from '<%= request.getContextPath() %>/js/index.js';
App('<portlet:namespace />-root');
</aui:script>
What changed?
- Switched from custom aui:script
requireto nativeimportsyntax
Wrapping Up
And that’s it! With just a few small updates, your project is
now running on modern JavaScript tooling. You’re free to choose your
favorite build tool, whether
it’s esbuild, vite,
or webpack or some other tool, and take full advantage of
faster builds, better plugin ecosystems, and a more intuitive
developer experience.
Happy coding, and welcome to the modern web! 🎉


