This is the legacy documentation of Project-level Custom Applications, which is in maintenance mode. Visit the new documentation for Org-level Custom Applications.
Example Deployment with Firebase
This deployment example refers to Firebase Hosting.
Prerequisites
Before you get started, you need to have:
A commercetools account and a project.
In this example we are going to use the commercetools platform running on the Europe cloud region on Google Cloud.
A Google Cloud Platform (GCP) account
The Firebase CLI
Create a Firebase project
In the Firebase console click Add project, then select or enter a Project name.
If you have an existing Google Cloud Platform (GCP) project, you can select the project from the dropdown menu to add Firebase resources to that project.
(Optional) Edit the Project ID.
The project ID displays in publicly visible Firebase resources, for example as the default Hosting subdomain - projectID.web.app or projectID.firebaseapp.com. This is the only opportunity to change your project ID.
Click Continue.
(Optional) Set up Google Analytics for your project.
Click Create project (or Add Firebase, if you're using an existing GCP project).
Initialize your project
To connect your local project to your Firebase project, run the following command from the root of your local project directory:
firebase init
During project initialization, from the Firebase CLI prompts:
Select to set up Hosting and Functions.
Select the Firebase project, which you have created in the previous section, to connect it to your local project directory.
Select JavaScript as language for writing Cloud Functions.
Choose whether you would like to use ESLint and install dependencies with npm (both recommended) or if you want to manage dependencies in another way.
Select the default directory as your public root directory. This directory is
public
and all compiled files will be placed there.Configure your site as single-page app to automatically add rewrite configurations.
After initialization, Firebase automatically creates and adds two files to the root of your local app directory:
A
firebase.json
configuration file that lists your project configuration.A
.firebaserc
file that stores your project aliases.
To support Cloud Functions, Firebase also adds the following structure to your project:
.└── functions├── .eslintrc.json├── index.js├── node_modules/└── package.json
.eslintrc.json
: optional file containing rules for JavaScript linting.package.json
: npm package file describing your Cloud Functions code.index.js
: main source file for your Cloud Functions code.
Configuration
To start, we need to update the custom-application-config.json
file with the production URL (replace the [projectID]
with the real Firebase project ID):
{"name": "My app","entryPointUriPath": "my-route","cloudIdentifier": "gcp-eu","env": {"production": {"url": "https://[projectID].firebaseapp.com"}}}
To configure Firebase deployments, we need to use a firebase.json
file. Normally, it would look something like this:
{"hosting": {"public": "public","ignore": ["firebase.json", "**/.*", "**/node_modules/**"],"rewrites": [{ "source": "**", "destination": "/index.html" }],"headers": [{"source": "**/*.@(js.map|js|css|txt|html|png)","headers": [{ "key": "Cache-Control", "value": "s-maxage=31536000,immutable" }]},{"source": "**","headers": [{ "key": "Cache-Control", "value": "no-cache" }]},]}}
Some fields may vary based on your setup and requirements, for example public
, ignore
, etc.
However, that won't work as the Custom Application does not have an index.html
after building the production bundles.
To make it work, we need to compile the application first.
Compile the application
To be able to deploy the Custom Application to Firebase, the Custom Application needs to be compiled.
mc-scripts compile-html
The command above does what we need: it compiles the index.html
using the JavaScript bundle references (after running mc-scripts build
) and the runtime configuration. At this point the index.html
file is ready for production usage.
However, the Custom Application needs to instruct the User-Agent (the browser) to enforce certain security measures, using HTTP headers (done automatically by the application config).
Because of that, the firebase.json
function file cannot be defined statically. Instead, it needs to be generated programmatically when the Custom Application is built and compiled.
To achieve that, we need to implement a transformer function.
Generate firebase.json
function using a transformer function
The compile-html
command accepts an option transformer
which we can use to pass the filesystem path to our transformer function.
We assume that the transformer function is defined at the following location: ./config/transformer-firebase.js
.
mc-scripts compile-html \--transformer ./config/transformer-firebase.js
The purpose of the transformer function is to generate the final firebase.json
file given the compiled values passed to the function.
// Function signature using TypeScripttype TransformerFunctionOptions = {// The runtime environment specified within the application config.env: Json;// The compiled HTTP headers, including the Content-Security-Policy headers.headers: Json;// The final HTML content of the `index.html`.indexHtmlContent: string;}type TransformerFunction = (options: TransformerFunctionOptions) => void;
The main export of the file should be the transformer function.
module.exports = function transformer(options) {// ...}
With that in mind, we can implement the transformer function and write the firebase.json
config into the filesystem.
const fs = require('fs');const path = require('path');const rootPath = path.join(__dirname, '..');module.exports = ({ headers }) => {const config = {hosting: {public: 'public',ignore: ['firebase.json', '**/.*', '**/node_modules/**'],rewrites: [{ source: '**', destination: '/index.html' },],headers: [{source: '**/*.@(js.map|js|css|txt|html|png)',headers: [{ key: 'Cache-Control', value: 's-maxage=31536000,immutable' },],},{source: '**',headers: Object.entries({...headers,'Cache-Control': 'no-cache',}).map(([key, value]) => ({ key, value })),},],},};const target = process.env.FIREBASE_TARGET;if (target) {config.hosting.target = target;}fs.writeFileSync(path.join(rootPath, 'firebase.json'),JSON.stringify(config, null, 2),{ encoding: 'utf8' });};
Adding fallback routes
This step is optional and does not prevent the application to be used within the Merchant Center. However, it's recommended to do so to avoid unexpected behaviors in case the URL, where the Custom Application is hosted, is accessed directly.
Accessing the Custom Application directly at https://[projectID].firebaseapp.com
won't work, as the application requires the user to log in and thus tries to redirect to the /login
route at the same domain.
To prevent that, we can add a dummy fallback route for the login|logout
routes. This is only meant to inform the user that the Custom Application cannot be used standalone.
exports.customAppFallback = functions.https.onRequest((req, res) => {res.end('This is not a real route. If you are seeing this, you most likely are accessing the Custom Application\n' +'directly from the hosted domain. Instead, you need to access the Custom Application from within the Merchant Center\n' +'domain, as Custom Applications are served behind a proxy router.\n' +'To do so, you need to first register the Custom Application in Merchant Center > Settings > Custom Applications.');});
This route will be used as a serverless function:
const fs = require('fs');const path = require('path');const rootPath = path.join(__dirname, '..');module.exports = ({ headers }) => {const config = {hosting: {public: 'public',ignore: ['firebase.json', '**/.*', '**/node_modules/**'],rewrites: [{ source: '**', destination: '/index.html' },{ source: '/@(login|logout)', function: 'customAppFallback' },],headers: [{source: '**/*.@(js.map|js|css|txt|html|png)',headers: [{ key: 'Cache-Control', value: 's-maxage=31536000,immutable' },],},{source: '**',headers: Object.entries({...headers,'Cache-Control': 'no-cache',}).map(([key, value]) => ({ key, value })),},],},};const target = process.env.FIREBASE_TARGET;if (target) {config.hosting.target = target;}fs.writeFileSync(path.join(rootPath, 'firebase.json'),JSON.stringify(config, null, 2),{ encoding: 'utf8' });};
Set up a Hosting Site (optional)
You can set up one or more Firebase Hosting sites in a single Firebase project. This is useful if you would like to host multiple Custom Applications on a single Firebase/GCP project.
Add an additional site directly from your Firebase Hosting page.
When you have multiple sites and you run Firebase CLI deploy commands, the CLI needs a way to communicate which settings should be deployed to each site.
With deploy targets you can uniquely identify a specific site by its target name in your firebase.json
configuration file and in your Firebase CLI commands for testing or deploying to your sites.
To create a deploy target and apply a target name to a Hosting site, run the following CLI command from the root of your project directory:
firebase target:apply hosting target-name resource-name
Where the parameters are:
target-name: a unique identifier (that you've defined yourself) for the Hosting site that you're deploying to.
resource-name: the name of the Hosting site as listed in your Firebase project.
The settings for deploy targets are stored in the .firebaserc
file in your project directory, so you only need to set up deploy targets once per project.
If you have configured a deploy target in a previous section, prefix the command compile-html
with the cross-env FIREBASE_TARGET=[target-name]
, where target-name
is the unique identifier previously defined.
The assets compiled into the public
directory will be deployed to Firebase.
Deployment
Finally, we can trigger the deployment using the Firebase CLI:
yarn buildmc-scripts compile-html \--transformer ./config/transformer-firebase.jsfirebase deploy
Now you're ready to Register your Custom Application and start using it!