BETA

Example Deployment with Firebase

This deployment example refers to Firebase Hosting.

Prerequisites

Before you get started, you need to have:

Create a Firebase project

  1. 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.

  2. (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.

  3. Click Continue.

  4. (Optional) Set up Google Analytics for your project.

  5. 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:

  1. Select to set up Hosting and Functions.

  2. Select the Firebase project, which you have created in the previous section, to connect it to your local project directory.

  3. Select JavaScript as language for writing Cloud Functions.

  4. 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.

  5. Select the default directory as your public root directory. This directory is public and all compiled files will be placed there.

  6. Configure your site as single-page app to automatically add rewrite configurations.

    Firebase Initialization

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 create an env.prod.json file with the following JSON:

{
"applicationName": "my-app",
"frontendHost": "mc.europe-west1.gcp.commercetools.com",
"mcApiUrl": "https://mc-api.europe-west1.gcp.commercetools.com",
"location": "gcp-eu",
"env": "production",
"cdnUrl": "https://[projectID].firebaseapp.com",
"servedByProxy": true
}

We also need a headers.prod.json to configure the Content Security Policy to allow the required hostnames:

{
"csp": {
"script-src": ["[projectID].firebaseapp.com"],
"connect-src": [
"[projectID].firebaseapp.com",
"mc-api.europe-west1.gcp.commercetools.com",
"mc-api.commercetools.com"
],
"style-src": ["[projectID].firebaseapp.com"]
}
}

The [projectID] should be replaced with your real Firebase project ID.

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

The Merchant Center Custom Applications are available by default with a built-in HTTP server, which takes care of preparing the index.html according to the env.json and headers.json configuration (see Runtime configuration).

To be able to deploy the Custom Application to AWS Cloudfront, the application needs to be configured and built statically. This is possible using the compile-html command.

mc-scripts compile-html

The command requires to provide the runtime configuration files so that the index.html can be properly compiled.

mc-scripts compile-html \
--headers=$(pwd)/headers.prod.json \
--config=$(pwd)/env.prod.json \
--use-local-assets

The --use-local-assets option is required for the sake of this example. See Serving static assets.

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. The HTTP headers are also compiled together with the index.html, as they rely on the runtime configuration headers.json.

Because of that, the firebase.json function file cannot be defined statically. Instead, it neeeds 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 \
--headers=$(pwd)/headers.prod.json \
--config=$(pwd)/env.prod.json \
--use-local-assets \
--transformer $(pwd)/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 TypeScript
type TransformerFunctionOptions = {
// The content of the `env.json` file.
env: Json;
// The compiled HTTP headers, including CSP (see `loadHeaders` from `@commercetools-frontend/mc-html-template`).
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.

transformer-firebase.jsJavaScript
module.exports = function transformer(options) {
// ...
}

With that in mind, we can implement the transformer function and write the firebase.json config into the filesystem.

./config/transformer-firebase.jsJavaScript
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.

./functions/index.jsJavaScript
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:

./config/transformer-firebase.jsJavaScript
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.

Firebase Hosting

Firebase Hosting Site

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.

Firebase Hosting Console

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 build
mc-scripts compile-html \
--headers=$(pwd)/headers.prod.json \
--config=$(pwd)/env.prod.json \
--use-local-assets \
--transformer $(pwd)/config/transformer-firebase.js
firebase deploy

Now you're ready to Register your Custom Application and start using it!