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! 🎉

