Template Extensibility

Template extensibility is a feature introduced in PWA Kit v3. The goal of this feature is to empower you to build PWA Kit projects more easily by customizing templates. You can customize existing base templates, such as the Retail React App, or you can create your own. We encourage members of the PWA Kit community to share their base templates with each other.

Template extensibility helps you modify a chosen template without having to duplicate every file within it.

Template extensibility is an optional feature. However, new projects generated after June 15, 2023 are automatically configured to use template extensibility.

In PWA Kit v3, a project can have a base template and an overrides directory, both of which can be configured in the project’s package.json file. To use template extensibility, you must declare a base template with ccExtensibility.extends in package.json and you must declare an overrides directory with ccExtensibility.overridesDir. (Configuration details are covered in the next section.)

A base template is an npm module that contains a fully functional PWA Kit project that has been preconfigured so that another PWA Kit project can override some of its files. Sometimes a base template is also called an "extensible app" or an "extendable app."

The overrides directory is a directory within your project where you can store files that override corresponding files in the base template.

Assume that you've already defined an override directory and defined the Retail React App as your base template. Let's say that you want to override the home page component of the Retail React App. In the Retail React App, the code for the home page component is in app/pages/home/index.jsx within the @salesforce/retail-react-app package. To override that file, you must recreate that same file in the directory declared via ccExtensibility.overridesDir in package.json. Therefore, the path for the new file is <ccExtensibility.overridesDir>/app/pages/home/index.jsx.

Now, whenever index.jsx is imported into any of the files within the @salesforce/retail-react-app package, the file in the overrides directory is loaded instead of the file in the package. Only the base of the filename needs to be the same for the override to happen. The file extensions can be different.

As you build your app, you can gradually add files from the base template to your overrides directory.

The more files that you override, the more effort is required to keep up with changes in the base template.

To see template extensibility in action, generate a fresh PWA Kit v3 project by running npx @salesforce/pwa-kit-create-app@latest --outputDir <path/to/new/local/project>.

The generated project uses Retail React App base template, which gives you an app that looks mostly the same as the one found at https://pwa-kit.mobify-storefront.com/.

In the template extensibility tutorials you’ll learn how to add some common customizations to your site:

The instructions in this section assume that you want to enable template extensibility for an existing project that wasn't generated with the feature already enabled.

To define a base template, add @salesforce/retail-react-app (or a different template) as an npm dependency in your project's package.json file.

Then add a ccExtensibility key to package.json with the following keys and values:

If you’re using a base template other than the Retail React App, don't forget to replace @salesforce/retail-react-app with the other template's package identifier.

You can replace the values for overridesDir with a custom directory name, but we recommend using overrides for the sake of consistency among projects (and our example code).

The same dependency can't be in both the main project and in the base template. The two PWA Kit apps must be different for @salesforce/pwa-kit-dev to differentiate between the main project and the base template. (The main project must define a base template and cannot inherit from itself.)

Your project must not have version clashes with the base template's npm dependencies listed in its package.json file. If the underlying template uses @chakra-ui, your project must also have it as a dependency, and it must be the same version. This precaution prevents accidental bloat in your bundle (not to mention functional breakage) due to conflicting versions of the same package. There is one exception to this rule: When you have added overrides for all of the files in the base template that imports a given dependency. In that case, the base template’s version of the dependency is never imported in your project because you have eliminated all files that import it via overrides.

Adding additional npm dependencies that are not used in the underlying template is just fine. 👌

Every PWA Kit project deployed to Managed Runtime must have the following files:

  • <overridesDir>/app/main.jsx
  • <overridesDir>/app/ssr.js
  • <overridesDir>/app/routes.jsx
  • <overridesDir>/app/request-processor.js
  • config/default.js (exception: if you are using the mobify key in package.json to store your site config data)

To customize behaviors in the project, it is common to override these files in @salesforce/retail-react-app:

  • app/pages/home/index.jsx: Removing the default Salesforce marketing content from the home page is a first step almost all projects.
  • app/static/*: These files are defaults that serve icons used by desktop and mobile browsers to brand your site. Update them to match your brand.
  • app/constants.js
  • app/assets/svg/brand-logo.svg: This file is overridden in the generator project. Follow this example to override other icons in your project.

The intent of this "filesystem as an API" setup is to help you find the underlying files, copy them, then customize the behavior and logic. Breaking down code into smaller files that orchestrate many subcomponents makes it easier to target only the behavior that you intend to override.

That being said, you sometimes have to copy a file into your overrides to change only a portion of the behavior of that file. Whenever possible, it's best to import the underlying template exports and re-export them so that changes to the template version don't require manual code updates in your extended project.

For example, in the routes.jsx file in a generated project, notice that the default routes are imported via import {routes as _routes} from '@salesforce/retail-react-app/app/routes' with _routes intended as a convention to indicate that these default routes are managed by an external dependency. Just like the file @salesforce/retail-react-app/app/routes.jsx, our custom implementation of routes.jsx exports routes; it adds a / (Home) route. The new route takes precedence over the default route because React Router finds the first item of a given pathname in the routes array.

Relative imports are the best approach for most projects that use template extensibility. The import behavior for the files in your base template (for example, @salesforce/retail-react-app) has special logic that gives precedence to the file in your overrides directory. This import logic does not apply to the files in your overrides directory. Therefore, we recommend using relative imports for most implementations.

Template extensibility can be complex to think about in terms of the "mental model", but an unexpected "gotcha" that can come up is accidentally importing two different conflicting files. A PWA Kit project is built via @salesforce/pwa-kit-dev, which uses webpack internally. There is a plug-in in @salesforce/pwa-kit-dev that enables the "override" functionality of template extensibility. This plug-in inspects all webpack file requests and asks two questions:

  1. Does this file request originate in the base template?
  2. Does this file exist in the overrides directory?

If the answer to both questions is "yes", the file request is rewritten at build time to point to the file in the overrides directory.

This logic does not come into play for webpack file requests that originate in the overrides directory. As such, it is possible to import the same file from both the overrides directory and the base template. If this situation happens with, for example, @saleforce/retail-react-app/my-file and <overrides directory>/my-file being imported in the same bundle, webpack throws a confusing error along the lines of can’t find <export name> from <filename>. Webpack throws this error because the bundle doesn’t know which import is the intended target for bundle inclusion.

It’s important to avoid importing files from the base template when an equivalent file exists in overrides, with one notable exception. As covered earlier in the code example in the Special Files section and demonstrated with constants.js, it's better to import the underlying template and re-export all of its exports, overriding or appending as few exports as are needed.

When you need to override the app/components/_app/index.jsx component, there are many global components, such as Header, Footer, and DrawerMenu. After the _app/index.jsx override is in place, any attempts by the base template to import this component will reroute the file request to <ccExtensibility.overridesDir>/app/components/_app/index.jsx. But there’s a catch! Because Header and Footer are imported by app/components/_app/index.jsx, the import must be updated in _app/index.jsx too! Otherwise, _app/index.jsx imports Header and Footer from the base template.

ECMAScript imports originating in ccExtensibility.extends are "magical" in that @salesforce/pwa-kit-dev checks in multiple places for a file of that name, with ccExtensibility.overridesDir taking precedence.

On the other hand, imports originating in <ccExtensibility.overridesDir>/* do not have magical behavior, thus you must target the files you mean to, and so <ccExtensibility.overridesDir>/app/components/header/index.jsx must be imported explicitly in <ccExtensibility.overridesDir>/app/components/_app/index.jsx like this:

Do not import from the package like this because it bypasses the header in your overrides directory:

Take note that routes.jsx is a special file. It forms the "entryPoint," in webpack terminology, for the entire application. If you have anything set up incorrectly in routes.jsx, it causes your whole app to fail in the compile phase as the route-based chunking in pwa-kit-dev does not resolve properly. For this reason, we include an example of how to properly extend routes properly by mixing imports from the default @salesforce/retail-react-app/app/routes.jsx and a local routes.jsx in the overridesDir.

Because the imports here are at "the top of the tree", if you override these files and then override footer.jsx, you must come back to _app/index.jsx and modify that import to point to your relative template import.

A known limitation of the template extensibility system is that, by virtue of magically pulling in, for example retail-react-app/constants, anytime the base template (@salesforce/retail-react-app) imports this file, it expects the same values to be exported.

A file in overrides that fails to export the same ECMAScript exports as its equivalent in the base template can cause unexpected errors like the one below.

Adding a constants.js file to your overrides directory like this causes an error:

Example error message:

The reason for this error is that CAT_MENU_DEFAULT_ROOT_CATEGORY is an expected export in @salesforce/retail-react-app/components/_app/index.jsx and in the process of overriding it with the sole export CUSTOM_MESSAGE above, your override breaks the underlying "API contract" (the ability of files to depend upon a given value, in this case CAT_MENU_DEFAULT_ROOT_CATEGORY) being exported by constants.js

The correct approach would be as follows. This approach avoids the omission of CAT_MENU_DEFAULT_ROOT_CATEGORY as a required and expected export. Thus we maintain the consistency of the exported API.

This approach only works for additive changes. In the following example, we add a CUSTOM_MESSAGE export that is not in the underlying retail-react-app/constants.js file’s exports.

This approach works for the following scenario, where DEFAULT_LOCALE is exported by retail-react-app/constants, and we are mutating that value:

Template hooks are a new way to interact with the Retail React App template that has historically been available via the command npx pwa-kit-create-app (now renamed to npx @salesforce/pwa-kit-create-app).

As part of the template extensibility feature, new "template hooks" have been added to @salesforce/retail-react-app that support global addition of a small subset of components, enumerated in the following list. By default, each of these components return null and is intentionally empty in order to avoid adding unneeded bloat to a completed PWA Kit implementation project. The base @salesforce/retail-react-app never adds any functionality to these components. Template hooks always return null in order to ensure that customer implementations can "hook" into the template in these places to customize the project without having to override more files than necessary.

As of @salesforce/retail-react-app@1.0.0 the following template hooks are available:

  • app/components/_app/partials/above-header.jsx
  • app/pages/product-list/partials/above-page-header.jsx

At the time of PWA Kit v3's release, there is only one extensible base template that is publicly available: @salesforce/retail-react-app (more are expected later).

To be used as a base template, a PWA Kit project must be published to npm and all of its ECMAScript imports must be prefixed with a value that matches the package name. For example, @salesforce/retail-react-app is published on npm, and its imports are prefixed with @salesforce/retail-react-app. This prefix is a reference to the package's root directory, which is necessary for it to work properly as a base template.

To use a PWA Kit project as an extensible base template, it must use <npm package name> for all of its imports. For example, @salesforce/retail-react-app adds ccExtensibility in package.json, which ensures that local IDE references (for example, @salesforce/retail-react-app/app/components/_app) resolve properly.

  • Use template extensibility instead of copying the Retail React App template and editing it.
  • As you make changes, test your project regularly to identify and fix any issues that come up.
  • Test the customized functionality in a non-production environment before deploying it to production.
  • Iterate on your overrides as needed to refine the behavior.

This section provides suggested solutions for common errors that you might encounter while using template extensibility.

After you changed files in your project the updates don’t appear when you preview your site.

Cause: You didn’t restart your local server.

Suggested Solution: Run npm start then preview your site again.

This applies only if you’re using a version of PWA Kit older than version 3.

Cause: Your package.json file is missing the required ccExtensibility configuration.

Suggested Solution: Complete this Configuration in package.json.

Cause: Required files are missing from the the overrides directory (overridesDir).

Suggested Solution: Ensure that your project includes all the required files in the overrides directory. See Required Project Files.

Cause: There are conflicting versions of dependencies between the base template and your project

Suggested Solution: Ensure both the base template and your project use the same version of shared dependencies. See Version Conflicts.