Skip to content
ToolShedby Wasim Shaikh

Sharing JavaScript Libraries Across Pages: The JS Import Map Entry Client Extension (jsImportMapsEntry)

Wasim · 8 Jul 2026

The Problem

Imagine you've built three custom elements for a Liferay site — a distributor map, a promotions widget, and a support ticket form. All three are React applications. Without thinking twice, each one bundles its own copy of React inside its build output.

The result:

Page loads distributor-map.js  → downloads React (140kb)
Page loads promotions.js       → downloads React again (140kb)
Page loads support-form.js     → downloads React a third time (140kb)

Same library, downloaded three separate times, potentially at three different versions if the projects drifted apart. This is the exact problem the JS Import Map Entry client extension (jsImportMapsEntry) exists to solve.

Reading time: 11-13 minutes · Difficulty: Intermediate · Audience: Developers building custom elements, Frontend Engineers


What jsImportMapsEntry Actually Does

A client extension of type jsImportMapsEntry adds entries to Liferay's JS import map — a mapping table that tells the browser how to resolve a module specifier (the string inside an import statement) to an actual file URL.

Once registered, any page element can write:

import React from 'react';

And the browser resolves react to wherever your deployed client extension says it lives — without that element needing to bundle its own copy.

The critical distinction from globalJS: import map entries are not scoped. Anything registered in the import map becomes available system-wide, instance-wide, the moment it's deployed. But it's also not automatically loaded everywhere — the browser only fetches the resource when something on the page actually contains a matching import statement. This combination — globally available, but only loaded on demand — is what makes it more efficient than globalJS for shared dependencies.


When to Use jsImportMapsEntry vs. globalJS

Question Use globalJS Use jsImportMapsEntry
Do you need a script to just run on page load? ✅
Are you sharing a library across multiple custom elements? ✅
Do you need page-scoped or company-scoped control over where it runs? ✅
Do you want the browser to skip downloading it if nothing on the page uses it? ✅
Is this a one-off script (analytics snippet, polyfill)? ✅
Is this a shared dependency (React, a generated API client, a utilities file)? ✅

As Liferay's own documentation puts it: jsImportMaps should be reserved for truly global items that need to be available and consistent everywhere — React, core utilities, animation libraries — while globalJS remains the tool for page-scoped or company-scoped scripts that should simply execute.


Prerequisites

  • Liferay Workspace set up locally
  • Java installed (per your Liferay version's compatibility matrix)
  • Blade CLI installed
  • A running Liferay DXP instance to test against
  • Basic familiarity with ES6 import/export syntax

Step 1: Create the Client Extension Project

Inside your Liferay Workspace:

cd client-extensions
mkdir my-shared-library
cd my-shared-library

Step 2: Write the Shared Module

Create an assets folder with an index.js entry point. This is where you import the library you want to share, and re-export it:

mkdir assets

assets/index.js:

import jquery from 'jquery';
export default jquery;

This file uses the library — in this case jQuery — and exports it as the module's default export. Anything imported and re-exported here becomes accessible to whatever consumes the client extension later.

You can also export multiple named items from a single shared module, not just a single default:

// assets/index.js
export { default as continents } from './data/continents';
export { default as sayHello } from './util/sayHello';

This lets a single jsImportMapsEntry extension act as a shared utilities package, not just a single library wrapper.


Step 3: Configure client-extension.yaml

# client-extension.yaml
assemble:
  - from: build/static
    into: static

my-shared-library:
  bareSpecifier: jquery
  name: My Shared Library
  type: jsImportMapsEntry
  url: jquery.*.js

Field breakdown:

Field Purpose
assemble Copies build output from build/static into the deployable package's static/ folder
bareSpecifier Required. The exact string other code will use in its import statement. This is the "key" other modules use to look up the entry in the import map
name Human-readable label shown in the admin UI
type Must be exactly jsImportMapsEntry
url The path to the built JavaScript resource once deployed

Important: The bareSpecifier value must match exactly what consuming code writes in its import statement. If bareSpecifier: jquery, then any custom element or fragment that wants to use it must write import jquery from 'jquery' — not import $ from 'jquery-lib' or any variation.


Step 4: Set Up the Build Process

Unlike globalCSS or globalJS, a jsImportMapsEntry extension typically needs a bundler (webpack or Vite) to compile the module and its dependencies into the build/static output your YAML references.

A minimal webpack.config.js for this purpose:

const path = require('path');

module.exports = {
  entry: './assets/index.js',
  output: {
    filename: 'jquery.[contenthash].js',
    path: path.resolve(__dirname, 'build/static'),
    library: {
      type: 'module',
    },
  },
  experiments: {
    outputModule: true,
  },
  mode: 'production',
};

The [contenthash] in the output filename is why the YAML uses a wildcard pattern (jquery.*.js) rather than a fixed filename — Liferay resolves the wildcard to whatever hash the build produced.

Alternative: Reference a CDN directly. If the library is already published on a CDN that serves ES modules, you can skip bundling entirely and point url straight at it:

vacation-react-router-import-maps-entry:
  bareSpecifier: react-router-dom
  name: React Router DOM Import Maps Entry
  type: jsImportMapsEntry
  url: https://esm.sh/react-router-dom@6.17.0?external=react

This is a common pattern for third-party libraries you don't need to customize — esm.sh and similar CDNs serve pre-built ES module versions of most popular npm packages.


Step 5: Build and Deploy

npm install
npm run build
blade gw deploy

For Docker-based local instances:

blade gw deploy -Ddeploy.docker.container.id=$(docker ps -lq)

Step 6: Consume the Shared Module

Once deployed, any custom element, fragment, or other client extension can import the shared module using its bareSpecifier:

import jquery from 'jquery';

jquery('.my-element').addClass('active');

From within a Liferay Fragment's JavaScript panel:

import 'clarity-distributor-map-custom-element';

This pattern — a custom element registering itself as a jsImportMapsEntry, and a fragment importing it by bareSpecifier — is how Liferay recommends exposing a custom element's resources to fragments and templates, instead of manually copying and pasting a generated resource URL every time the build hash changes.


Step 7: Verify It's Working

  1. Open your browser's DevTools → Network tab.
  2. Load a page containing an element that imports your shared module.
  3. Confirm the module downloads exactly once, even if multiple elements on the page reference it.
  4. Load a page that does not reference the module at all, and confirm it does not download — this confirms the "loaded only when referenced" behavior is working as expected.

If the module fails to resolve, work through this checklist:

  • Confirm bareSpecifier in the YAML exactly matches the string in your import statement
  • Confirm the extension deployed without errors
  • Confirm the build output actually landed in the path referenced by url
  • Hard-refresh the browser to bypass any cached import map

Real-World Example: One Shared Runtime, Multiple Consumers

Consider a distributor management portal with three separate custom elements, all built in React:

clarity-distributor-map        (React custom element)
clarity-promotions-widget      (React custom element)
clarity-support-form           (React custom element)

Without a shared import map, each bundles its own React runtime — three separate downloads, three separate copies in memory, and a real risk of the three drifting to different React versions over time as each is updated independently.

With a jsImportMapsEntry extension:

clarity-shared-react:
  bareSpecifier: react
  name: Clarity Shared React Runtime
  type: jsImportMapsEntry
  url: react.*.js

All three custom elements import React the same way:

import React from 'react';

The browser downloads React exactly once, caches it, and every subsequent custom element on the page reuses that same cached copy. Updating React means updating one client extension — the change applies everywhere instantly, with no risk of version drift between the three elements.


A Note on Styling: jsImportMapsEntry Only Bundles JavaScript

Unlike a full customElement client extension — which can bundle both JavaScript and CSS together — a jsImportMapsEntry extension only handles JavaScript. If the shared module needs accompanying styles, you'll need to either embed the CSS inline within the JavaScript itself, or pair it with a separate globalCSS client extension.


Common Mistakes

1. Mismatched bareSpecifier and Import Statement

# YAML
bareSpecifier: my-lib
// Incorrect — won't resolve
import lib from 'mylib';

// Correct — matches exactly
import lib from 'my-lib';

2. Assuming It Can Be Scoped Like globalJS

There is no scope property for jsImportMapsEntry. Once deployed, the entry is available instance-wide by definition — there's no page-level or company-level restriction to configure. If you need scoping, jsImportMapsEntry is the wrong tool.

3. Forgetting the Wildcard Pattern for Hashed Filenames

If your bundler outputs content-hashed filenames (a common cache-busting technique), your YAML's url needs a wildcard (react.*.js) rather than a literal filename that will change on every build.

4. Expecting CSS to Come Along for Free

As noted above, jsImportMapsEntry bundles JavaScript only. Teams migrating from customElement extensions sometimes assume styling transfers automatically — it doesn't.

5. Treating It as a Replacement for globalJS in All Cases

If your goal is simply "run this script when the page loads" — a tracking snippet, a polyfill, a one-off DOM manipulation — jsImportMapsEntry adds unnecessary complexity. It's built for sharing importable modules between other pieces of code, not for standalone execution.


What Gets Created Behind the Scenes

Component Result
Import map entry Registered instance-wide, keyed by bareSpecifier
Deployable package Bundled JS (or CDN reference) served as a static resource
Loading behavior Downloaded only when a page element contains a matching import statement
Caching Browser caches the module after first load; reused across every page and element that imports it
Scope Always instance-wide — no page or company-level restriction available

Summary

In this tutorial, you:

  1. Identified the problem of duplicate library bundling across multiple custom elements
  2. Learned how jsImportMapsEntry differs from globalJS — global-but-lazy versus scoped-but-immediate
  3. Created a shared module in assets/index.js
  4. Configured client-extension.yaml with the required bareSpecifier and url properties
  5. Set up a build process (or alternatively, referenced a CDN-hosted module directly)
  6. Deployed the extension and consumed it from other code via a standard import statement
  7. Walked through a real-world example of three custom elements sharing a single React runtime

The core idea worth remembering: jsImportMapsEntry doesn't push code onto every page — it makes code available to any page, loaded only when something actually asks for it. That distinction is what makes it the correct tool for shared dependencies, and the wrong tool for anything meant to simply run.


References

This tutorial draws on the following official Liferay Learn documentation and community resources:


Wasim Shaikh

About the author

Wasim Shaikh is a UI/UX developer and front-end engineer with 15+ years of experience, based in Ahmedabad, India. He specializes in Liferay, React, Angular, Next.js and Tailwind CSS.

Keep reading

Web dev · 8 Jul 2026Creating Your First Client Extension: A Step-by-Step Tutorial (globalJS)Web dev · 8 Jul 2026Creating Your First Client Extension: A Step-by-Step Tutorial (globalCSS)Web dev · 8 Jul 2026Creating Your First Custom Element: A Step-by-Step Tutorial (React Weather Card)