One of the promises of using a component-based UI framework is reusability. This is especially true for web component frameworks like Lightning Web Components (LWC). The newly released module resolution capability for LWC Open Source is not only for your local modules, it’s also for the distribution and consumption of third-party modules. This blog post will guide you through the different possible configurations, and how you can implement them for your own projects.

What is module resolution?

Module resolution is basically the process that helps to resolve any imports within your codebase. Let’s take a look at two basic examples:

// Component 1 - app.html
<template> <my-child></my-child>
</template> // Component 2 - apexChild.js
import { LightningElement } from 'lwc';
import someFunMethod from '@salesforce/apex/MyClass.someFunMethod'; export default class ApexChild extends LightningElement {

When you look at these (unrelated) markup and JavaScript examples, you will see two different imports. First, the HTML file refers to the my-child web component via markup. This reflects an import. Second, the JavaScript file imports an Apex method (Wait, what? On open source? Read on for more about that). This may be more what you think of as an import.

At build time the compiler has to figure out, based on these imports, where the web component and Apex method physically reside in the project to make the compilation work. That process is called module resolution.

Open source vs. Platform

Before I start digging into the configuration options, I want to start with the elephant in the room: module resolution as described here is only available for LWC Open Source projects, and not LWC in Salesforce DX projects. The reason for this is that the Salesforce Platform automatically provides the resolution mechanisms.

Module resolution and compilation on-platform is automatically handled for you at deployment-time, based on the metadata that is stored in your Salesforce organization. This is different from LWC Open Source projects, as the compilation happens locally.

With that covered, let’s take a look at how and where you can configure module resolution.

Module sources

As with other tools like ESLint or Prettier you have to configure where the module resolver will look for modules. For that you can either create a new lwc key within your package.json, or your create a dedicated lwc.config.json file within the root of your project directory (the latter is default on new create-lwc-app projects).

The LWC module resolver differentiates between three different types of module locations (also called ModuleRecords):

  • Directories
  • Aliases
  • npm

Here is an example configuration that illustrates these types:

{ "modules": [ { "dir": "src/modules" }, { "name": "@salesforce/apex/MyClass.someFunMethod", "path": "src/hacky/apex/myClassSomeFunMethod/index.js" }, { "npm": "lwc-recipes-oss-ui-components" } ]
}

The dir ModuleRecord entry instructs the module resolver to look for modules in the local folder src/modules, relative to the current project root. The resolver looks recursively for LWC modules within the given directory, and resolves their file names. Note that you will need at least one entry that points to your local source in all cases. As a result, this is also the default configuration when you create a new LWC Open Source project using create-lwc-app.

The second entry represents the alias ModuleRecord type. For illustration I’ve chosen an example that may look odd, because you cannot run Apex outside of the Salesforce Platform. In fact, that’s what makes it a good illustration example: The alias type can be used to explicitly define the component path that should be resolved for a given module name. So you can basically tell the module resolver to alias the Apex import on off-platform. How cool is that?

The npm ModuleRecord goes a bit further. Here you don’t specify a directory path, but instead the name of an npm package that you added to your project (as devDependency, please). Yes, you read correctly. This enables you to consume and distribute third-party LWC npm packages. This ModuelRecord type comes back to the initial promise of reusable components, because with it you are not limited to modules that are part of your local project’s source.

Let’s take a look at what you have to configure within your create-lwc-app project to create such a distributable package, based on the above example of lwc-recipes-oss-ui-components.

Prepare your components for external distribution

To distribute your own Lightning Web Component packages you have to complete two steps.

First, you have to explicitly define which components from your project you want to expose for consumption. You define them via the expose key in your lwc.config.json file.

{ "modules": [ { "dir": "src/modules" } ], "expose": [ "ui/button", "ui/card", "ui/input", "ui/navfooter", "ui/output", "ui/select" ]
}

If you are wondering why you have to explicitly define these components, it’s because you may have many more internal components that you may change or break. By explicitly defining them, you make sure that consumers can only use what you want them to consume. This step defines the API contract of your package.

The second step is to include your actual src folder (or whatever name you choose and configure for your source folder) with the npm package. The actual compilation of a code base happens locally on the consumer’s machine, and if you compile your components with a different LWC version, that may cause trouble.

And that’s really it. Almost; there’s one more thing.

Testing

As responsible developers, we test our components. It’s easy when you test your own projects’ components, as you own the source and test against your real components. The situation changes when you consume components from an npm package. In that case you have to stub them yourself.

To do this you have to create the stubs locally in your project. An example for how we built the stubs of the lwc-recipes-oss-ui-components package can be found in the LWC Recipes OSS sample app. After you create the stubs you have to configure Jest in your local project. That way it knows how to map the module names from the third-party npm package to your local stubs during testing. The engineering team is looking into options to make this a smoother experience, and you can join the conversation.

If you develop on the Salesforce Platform, and you are wondering how the base Lightning components are stubbed for local Jest testing within DX projects, take a look at the sfdx-lwc-jest repository.

Summary

The new module resolution for Lightning Web Components Open Source provides great flexibility for developers. It not only enables more flexible project configurations, it also makes it possible to build and distribute your own LWC npm libraries. You can read more about the implementation in the module resolution RFC; I encourage you to take a look.

Note that some of this may become obsolete once node 14 is broadly available, as it ships with a built-in mechanism to resolve internal package imports for ES6 modules.

A practical example can be found in this repository (including the aliased Apex module), and also in LWC Recipes OSS. For more on how LWC module resolution works, check out my CodeLive session.

About the author

René Winkelmeyer works as Architect, Developer Evangelism, at Salesforce. He focuses on enterprise integrations, Lightning, and all the other cool stuff that you can do with the Salesforce Platform. You can follow him on Twitter @muenzpraeger.

Subscribe To Our Newsletter

Subscribe To Our Newsletter

Join our mailing list to receive the latest news and updates from our team.

You have Successfully Subscribed!