How to use the Liferay global JS object for your local component JavaScript or TypeScript projects.
At Liferay DevCon today, someone asked me a great question:
“How do you develop and test a local custom element (a standard Web Component) while still using the Liferay JS global object?”
We talked it through, and I hinted I’d get a blog post up about it. Since I’m sure others will hit this same challenge… here we go.
Good news: it’s easier than you might think.
All you really need is a lightweight stub of the Liferay object (just enough of the API surface your custom element relies on) and then import that during local development.
Below is the simple Liferay.js file I use:
const Liferay = window.Liferay || {
Language: {
get: (key) => {
return key;
},
},
OAuth2: {
getAuthorizeURL: () => '',
getBuiltInRedirectURL: () => '',
getIntrospectURL: () => '',
getTokenURL: () => '',
getUserAgentApplication: (_serviceName) => {},
},
OAuth2Client: {
FromParameters: (_options) => {
return {};
},
FromUserAgentApplication: (_userAgentApplicationId) => {
return {};
},
fetch: (_url, _options = {}) => {},
},
ThemeDisplay: {
getCompanyGroupId: () => 20119,
getPathThemeImages: () => '',
getPortalURL: () => 'http://localhost:8080',
getScopeGroupId: () => 20117,
getSiteGroupId: () => 20117,
isSignedIn: () => false,
},
authToken: '',
on: (_event, _callback) => {},
fire: (_event, _data) => {},
};
export default Liferay;
This is not the complete Liferay global object; Liferay’s real runtime includes many more fields and service APIs, but this stub is perfect for local development and testing for the component I was working on at the time.
I included only the pieces I needed:
Liferay.Language.get()Liferay.ThemeDisplayfields for companyId, groupId, portal URL, etc.Liferay.OAuth2+Liferay.OAuth2Client- Event helpers (
on,fire) authToken
If tomorrow I needed something like Liferay.Session.extend(), I’d just add another stub to the same file.
TypeScript Version
If you’re using TypeScript, here’s a version you can drop into something like liferay.ts:
interface LiferayType {
Language: {
get: (key: string) => string;
};
OAuth2: {
getAuthorizeURL: () => string;
getBuiltInRedirectURL: () => string;
getIntrospectURL: () => string;
getTokenURL: () => string;
getUserAgentApplication: (serviceName: string) => unknown;
};
OAuth2Client: {
FromParameters: (options: Record<string, unknown>) =>
unknown;
FromUserAgentApplication: (userAgentApplicationId: string)
=> unknown;
fetch: (url: string, options?: Record<string, unknown>) =>
unknown;
};
ThemeDisplay: {
getCompanyGroupId: () => number;
getPathThemeImages: () => string;
getPortalURL: () => string;
getScopeGroupId: () => number;
getSiteGroupId: () => number;
isSignedIn: () => boolean;
};
authToken: string;
on: (event: string, callback: (...args: unknown[]) => void)
=> void;
fire: (event: string, data?: unknown) => void;
}
const Liferay: LiferayType =
(window as any).Liferay ||
({
Language: {
get: (key: string) => key,
},
OAuth2: {
getAuthorizeURL: () => '',
getBuiltInRedirectURL: () => '',
getIntrospectURL: () => '',
getTokenURL: () => '',
getUserAgentApplication: (_serviceName: string) => {},
},
OAuth2Client: {
FromParameters: (_options: Record<string, unknown>) =>
({}),
FromUserAgentApplication: (_id: string) => ({}),
fetch: (_url: string, _options?: Record<string, unknown>)
=> {},
},
ThemeDisplay: {
getCompanyGroupId: () => 20119,
getPathThemeImages: () => '',
getPortalURL: () => 'http://localhost:8080',
getScopeGroupId: () => 20117,
getSiteGroupId: () => 20117,
isSignedIn: () => false,
},
authToken: '',
on: (_event: string, _callback: (...args: unknown[]) =>
void) => {},
fire: (_event: string, _data?: unknown) => {},
} as LiferayType);
export default Liferay;
You'll find this one to be very useful since it provides the type-safe definition of the runtime object that Liferay doesn't provide otherwise.
Adding missing stubs to the TypeScript version does take slightly more effort here.
Because TypeScript enforces structure, you’ll want to make sure your interface matches the real Liferay global object, or at least the parts of it your component actually uses. That means:
- Matching method signatures (e.g., what parameters
Liferay.OAuth2Client.fetch()expects) - Matching return types
- Including any nested services or namespaces your component depends on
If you get it wrong, your component may type-check locally but throw errors once deployed into Liferay.
So where do you find the real definitions? The easiest way I've found is to use the Browser console: Open DevTools → type Liferay → expand it. I know it seems kind of odd, but when you see the Liferay source, the JS is scattered around and merged at runtime, so it can be hard to find the pieces that you need in there directly.
You don’t need to replicate the entire object, just enough for your component to run locally in a type-safe way.
Wrapping Up
This small pattern, a simple Liferay.js (or liferay.ts) stub, has been incredibly useful for me whenever I'm building local custom elements that rely on the Liferay global object.
It lets me:
- Develop and test locally at full speed
- Avoid spinning up a portal every time
- Use the
||syntax so my stub works locally but automatically defers to the realwindow.Liferaywhen deployed
Short and sweet, but very practical.
If you’ve ever struggled with local development for Liferay-integrated custom elements, I hope this makes your day just a bit easier.
Related Assets...
More Blog Entries...
One of the most important thing that I've learned reading this book, is that Continuous Integration (CI), is a practice, not a tool , and requires a significant degree of discipline from the team as a whole. So all team members are involved on it, and must collaborate to achieve its perfection.

The objective of a CI system is to ensure that a software is working, in essence, all of the time
We hope this blog serie helps you if you are starting with CI, but we also want to hear your experience and your feedback about. So please comment what you consider on it.
Ok, once introduced the topic, let's start with the first practice....
Practice 1: Don't check-in on a broken build

You are about to start a new work day, and see the build broken. Have you received an email from the CI server? If so, you should know how to proceed to verify if you are the causant of the errors, and if it is so, please try to solve it as soon as possible instead of starting to code that stellar functionality as a beast.
Doing that, you can identify the cause of the breakage very quick, and then fix it, because you are in the best position to work out what caused the breakage.
But, wait, you have already finished your work, and the build is still broken. Why shouldn't you check-in further changes on that broken build?
First of all, it will compound the failure with more problems. Imagine that you don't know about these practices, so every time you check-in, you cannot prove that your changes are not adding more errors, and maybe your changes plus existing errors cause another different problems.
Direct consequence of previous sentence is that it will take much longer for the build to be fixed, because you added more complexity to the problem.
Of course, you can still check-in. And you can also get used to seeing the build broken. In that case, the build stays broken all the time :(

And that's the cycle, it's true.
But after many broken builds, the long term broken build is usually fixed by an Herculean effort of somebody on the team (here in Liferay is usually Miguel) and the process starts again.

Ok, that's all for today.
I'm looking forward to hearing your feedback!!