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:
- Customize a Component — component-level extensibility
- Customize a Page — page-level extensibility
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 upgrade to the latest version of the Retail React App, run this command:
npm install @salesforce/retail-react-app@latest
After running that command, we recommend that you test your site to confirm that it works as expected.
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).
Don’t list the same npm dependencies in both the main project and the base template. Confirm that the dependencies listed in your project's package.json
are different from those in the base template. This ensures that @salesforce/pwa-kit-dev
can differentiate between the two in dependency resolution. Define a base template in the main project’s package.json
as described under Configuration. The main project can’t 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 themobify
key inpackage.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:
- Does this file request originate in the base template?
- 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.