Module Providers for Lightning Web Runtime on Node.js
Module providers locate and serve modules for your web app. Your project’s LWR server contains a module registry, which maintains a list of available module providers and helps match module requests to a valid module provider.
All Lightning Web Runtime on Node.js (LWR-JS) apps automatically include default module providers that support common module types, like static file system modules and packaged Lightning web components (LWCs). If you want to use ECMAScript (ES) modules from other sources, you can import a module provider or create a custom module provider.
This flexibility ensures that your runtime is small for simple use cases, but LWR-JS can support more complex applications if needed.
Default Module Providers
Your project automatically includes these module providers.
1'@lwrjs/app-service/moduleProvider', // Application Bootstrap Module Provider
2'@lwrjs/router/module-provider', // Router Module Provider
3'@lwrjs/lwc-module-provider', // LWC Module Provider
4'@lwrjs/npm-module-provider', // Node Package Manager (npm) Module Provider
5'@lwrjs/module-registry/externals-module-provider' //Application Bootstrap Module Provider
This module provider generates only the Application Bootstrap Module. You need it for your website to load correctly.
Router Module Provider
The Router Module Provider generates a router based on a static JSON file.
LWC Module Provider
The LWC Module Provider locates and serves Lightning Web Components (LWC) modules for your app. LWCs are special instances of ES modules that extend the LightningElement class. They have to be processed by the LWC compiler before a module provider can serve them.
npm Module Provider
This module provider locates and serves all of the Node Package Manager (npm) ES modules from your project’s node_modules directories.
Externals Module Provider
The LWR-JS Externals Module Provider creates module definitions based on the externals in your project’s bundle configuration (bundleConfig).
Import a Module Provider
With LWR-JS, you can add an existing module provider to your project. For example, the LWR Label Module Provider exposes capabilities for scoped module label support (@salesforce/label/*). You can add this tooling to your application via the @lwrjs/label-module-provider npm package.
To add an optional module provider to your project, first install the package using npm on the command line interface (CLI). For example, this code installs the Label Module Provider.
1npm install @lwrjs/label-module-providerThen, add the module provider plugin to your lwr.config.json file. Remember to include the default module providers in the moduleProviders array. Here’s how to add the Label Module Provider to your app’s config file.
The moduleProviders array overwrites the default LWR array. You have to list all the module providers needed by your app, including the default LWR ones listed in Default Module Providers.
Important
1{
2 "moduleProviders": [
3 "@lwrjs/label-module-provider", // Optional provider
4 "@lwrjs/app-service/moduleProvider", // Default provider
5 "@lwrjs/router/module-provider", // Default provider
6 "@lwrjs/lwc-module-provider", // Default provider
7 "@lwrjs/npm-module-provider", // Default provider
8 "@lwrjs/module-registry/externals-module-provider" // Default provider
9 ]
10}That configuration lets your application components statically import labels sourced from JSON files in your project’s $rootDir/src/labels directory, like this:
1import GREETING from "@salesforce/label/home.greeting";Optional Supported LWR-JS Module Providers
You can add these module providers to your LWR-JS project.
Create a Custom Module Provider
If you want your app to use a custom module that can’t be served by one of the default module providers, add a custom module provider to your project. You can create a custom ES provider or a custom LWC provider.
In this section, we’ll walk through how to create and implement a custom ES provider. We can boil this process down into three steps.
- Create the module provider.
- Configure
getModuleEntry()andgetModule(). - Register the module provider.
Create a Custom ES Module Provider
To create an ES module provider, implement the ModuleProvider LWR interface. You have to give the module provider a name, but the version property is optional.
1import { ModuleProvider } from 'lwr';
2export default class MyProvider implements ModuleProvider {
3 name = 'echo-provider';
4 private version = '1';
5}Save this file in your app’s src folder. For example, the sample module provider es-custom-provider is nested in src/services.
1build/
2node_modules/
3src/
4 ├── modules/
5 ├── services/
6 │ ├── es-custom-provider.ts
7 │ └── lwc-custom-provider.ts
8 └── index.ts
9lwr.config.json
10package.jsonConfigure getModuleEntry() and getModule()
Module providers rely on getModuleEntry() and getModule() to match module requests to module providers and help compile modules. You should customize these functions for your custom module provider.
getModuleEntry()
When your LWR server receives a request for a module – like someone loading a webpage – the module registry calls getModuleEntry() on all of its module providers. The first module provider to correctly handle this request is assigned to serve the requested module.
The module registry passes these two arguments into getModuleEntry().
- A module ID, which contains the module’s
specifier(a unique ID value). RuntimeParams, a map of unique requirements for the requested page. For example, it could include a requirement for a specificlocale.
The function returns a ModuleEntry object that contains information from a valid module provider and the module request.
You should customize the information in ModuleEntry based on your custom module provider and the module it serves.
id- At a minimum, include the module’sspecifierand the module provider’sversion. If the module is locale-specific, include thelocalevalue fromRuntimeParams.virtual- If module code from your provider is generated from scratch, set this totrue. If it’s read off of a file system or another source, set this tofalse.entry- Set this to the module’s filepath. If the module is virtual, ensure the path includes thespecifier.version- Set this to the module provider’s version.
Here’s an example of a ModuleEntry object returned by a custom module provider.
1import { AbstractModuleId, ModuleCompiled, ModuleEntry, ModuleProvider, RuntimeParams } from 'lwr';
2
3export default class MyProvider implements ModuleProvider {
4 async getModuleEntry(
5 { specifier }: AbstractModuleId,
6 runtimeParams: RuntimeParams = {},
7 ): Promise<ModuleEntry | undefined> {
8 if (specifier.startsWith('@my/')) { // Module provider checks for the @my namespace
9 return {
10 id: `${specifier}|${this.version}`,
11 virtual: true,
12 entry: `<virtual>/${specifier}.js`,
13 specifier, // Reuse the specifier passed into getModuleEntry()
14 version: this.version,
15 };
16 }
17 }
18}getModule()
When the module registry validates that a module provider can fulfill a module request, the registry calls getModule(). getModule() takes the same arguments as getModuleEntry(). It returns a ModuleCompiled object.
1import { AbstractModuleId, ModuleCompiled, ModuleEntry, ModuleProvider, RuntimeParams } from 'lwr';
2import { hashContent } from '@lwrjs/shared-utils';
3
4// Return a generated ES code string
5function generateModule(specifier: string): string {
6 return `...`;
7}
8
9export default class MyProvider implements ModuleProvider {
10 async getModule(
11 { specifier, namespace, name }: AbstractModuleId,
12 runtimeParams: RuntimeParams = {},
13 ): Promise<ModuleCompiled | undefined> {
14 // Retrieve the Module Entry
15 const moduleEntry = await this.getModuleEntry({ specifier });
16 if (!moduleEntry) {
17 return;
18 }
19
20 // Generate code for the requested ES module
21 const originalSource = generateModule(specifier);
22
23 // Construct a Module object
24 return {
25 id: moduleEntry.id,
26 specifier,
27 namespace,
28 name,
29 version: this.version,
30 originalSource,
31 moduleEntry,
32 ownHash: hashContent(originalSource),
33 // Note: there is no need to compile this module
34 // The module registry will compile ES code, if needed
35 compiledSource: originalSource,
36 };
37 }
38}Register a Custom Module Provider
To register a custom module provider, add it to your project’s lwr.config.json file. Make sure that TypeScript has been transpiled into JavaScript.
1{
2 "moduleProviders": [
3 "$rootDir/build/services/es-custom-provider.js",
4 "$rootDir/build/services/lwc-custom-provider.ts",
5 "@lwrjs/app-service/moduleProvider",
6 "@lwrjs/lwc-module-provider",
7 "@lwrjs/npm-module-provider"
8 ]
9}The moduleProviders array overwrites the default LWR array. You have to list all the module providers needed by your app, including the default LWR ones listed in Default Module Providers.
Important
Alternatively, you can pass your configuration from lwr.config.json to a module provider constructor. The configuration can be any JSON object or primitive type.
1{
2 "moduleProviders": [
3 [
4 "$rootDir/build/services/provider-with-config.js",
5 {
6 "cache": true,
7 "locales": ["en", "es", "de"]
8 }
9 ]
10 ]
11}The configuration and ProviderContext (from the LWR server) are passed into the module provider constructor.
1interface MyProviderOptions {
2 cache?: boolean;
3 locales?: string[];
4}
5export default class MyProvider implements ModuleProvider {
6 constructor({ cache = true, locales = [] }: MyProviderOptions, context: ProviderContext) {
7 // initialization
8 }
9}Create a Custom LWC Module Provider
LWCs are special instances of ES modules that extend the LightningElement class. Before a module provider serves LWCs, they have to be processed by the LWC compiler.
The easiest way to create a custom LWC module provider is to extend the LwcModuleProvider class from the package @lwrjs/lwc-module-provider.
To adapt the LWC module provider for your LWR app, override getModuleEntry() and getModuleSource(). The getModule() function of the superclass handles compilation of the LWC.
1import path from 'path';
2import LwcModuleProvider from '@lwrjs/lwc-module-provider'; // add to package.json dependencies
3import {
4 AbstractModuleId,
5 FsModuleEntry,
6 ModuleCompiled,
7 ModuleEntry,
8 ModuleProvider,
9 ModuleSource,
10} from 'lwr';
11import { hashContent } from '@lwrjs/shared-utils';
12
13// Return generated LWC code strings by file type: html, css or default js
14function generateModule(specifier: string): string {
15 const fileType = path.extname(specifier).substring(1);
16 switch (fileType) {
17 case 'html':
18 return `<template><!-- HTML code --></template>`;
19 case 'css':
20 return `/* CSS code */`;
21 default:
22 // 'js'
23 return `
24import { LightningElement } from 'lwc';
25export default class MyLwc extends LightningElement { /* LWC code */ }`;
26 }
27}
28
29export default class MyLwcProvider extends LwcModuleProvider implements ModuleProvider {
30 name = 'lwc-provider';
31 private version = '1';
32
33 async getModuleEntry({ specifier }: AbstractModuleId): Promise<FsModuleEntry | undefined> {
34 if (canHandle(specifier)) {
35 return {
36 id: `${specifier}|${this.version}`,
37 // Incoming specifiers may be html or css code
38 // Ensure the entry file extension matches:
39 entry: `<virtual>/${specifier}${path.extname(specifier) ? '' : '.js'}`,
40 specifier,
41 version: this.version,
42 };
43 }
44 }
45
46 getModuleSource(
47 { name, namespace, specifier }: AbstractModuleId,
48 moduleEntry: ModuleEntry,
49 ): ModuleSource {
50 const originalSource = generateModule(specifier);
51 return {
52 id: moduleEntry.id,
53 specifier,
54 namespace,
55 name: name || specifier,
56 version: moduleEntry.version,
57 moduleEntry,
58 ownHash: hashContent(originalSource),
59 originalSource,
60 };
61 }
62
63 // This method handles LWC compilation => let the superclass handle this processing
64 // It calls `getModuleSource` under the covers
65 async getModule(moduleId: AbstractModuleId): Promise<ModuleCompiled | undefined> {
66 return super.getModule(moduleId);
67 }
68}Developer Preview Feature