17

The Scenario:

I'm building an Ionic3 application, and my config.xml have some data that I want to be able to change according to my environment (for example, I want that my facebook app id have different values for development, staging and production).

I achieved this creating a template config.xml (the file is config.tpl.xml) and a before_prepare cordova hook to replace the variables in the template with the correct values and save the generated content in config.xml.

The cordova hook uses the npm package es6-template-strings:

npm install es6-template-strings --save-dev

The hook is:

#!/usr/bin/env node
var fs = require('fs');
var path = require('path');
var compile = require('es6-template-strings/compile');
var resolveToString = require('es6-template-strings/resolve-to-string');

var ROOT_DIR = process.argv[2];
var FILES = {
    SRC: "config.tpl.xml",
    DEST: "config.xml"
};

var env = process.env.NODE_ENV || 'dev';
var envFile = 'src/environments/environment.' + env + '.json';

var srcFileFull = path.join(ROOT_DIR, FILES.SRC);
var destFileFull = path.join(ROOT_DIR, FILES.DEST);
var configFileFull = path.join(ROOT_DIR, envFile);

var templateData = fs.readFileSync(srcFileFull, 'utf8');

var configData = fs.readFileSync(configFileFull, 'utf8');
var config = JSON.parse(configData);

var compiled = compile(templateData);
var content = resolveToString(compiled, config);

fs.writeFileSync(destFileFull, content);

I have files in the src/environments/ directory for different environments, that are chosen based on the NODE_ENV value that is defined when I build cordova. For example, if NODE_ENV=prod (production), then it would use the file environment.prod.json:

{
    ...
    "FACEBOOK_APP_ID": "11111111",
    "FACEBOOK_APP_NAME": "My Facebook App Name",
    ...
    "PUSH_SENDER_ID": "22222222",
    ...
}

When the hook is executed, this part in the cordova.tpl.xml:

<plugin name="cordova-plugin-facebook4" spec="~1.7.4">
    <variable name="APP_ID" value="${FACEBOOK_APP_ID}" />
    <variable name="APP_NAME" value="${FACEBOOK_APP_NAME}" />
</plugin>
<plugin name="phonegap-plugin-push" spec="~1.9.2">
    <variable name="SENDER_ID" value="${PUSH_SENDER_ID}" />
</plugin>

becomes like:

<plugin name="cordova-plugin-facebook4" spec="~1.7.4">
    <variable name="APP_ID" value="11111111" />
    <variable name="APP_NAME" value="My Facebook App Name" />
</plugin>
<plugin name="phonegap-plugin-push" spec="~1.9.2">
    <variable name="SENDER_ID" value="22222222" />
</plugin>

The problem:

So far so good. The problem is that when some automatic changes are done to config.xml (like adding plugins), it is not reflected in cordova.tpl.xml, so I have to remember to make the changes manually.

Although the way it is done now is still much better than having to add each environment variable like before (it was a maintenance nightmare and error prone), I still have to change the template every-time a add/change/remove a plugin (not so frequent, and easy to discover the problem when I forget it, but still far from the ideal).

My question:

I would like to know if there is a way avoid these manual changes, but keep using the environment variables like I'm doing now.

It could be achieved making the automatic changes to config.xml to be done to config.tpl.xml instead (like when adding a plugin with --save), if possible, but I haven't found any cordova option about this.

Or using the config.xml as the template and send it to another place with the variables defined (e.g. to www/config.xml), and use the config.xml in the other location to be used to build the app (not the config.xml in the root, i.e., the template). I would only change the src and dest files in my hook (to config.xml and www/config.xml, respectively). But I also haven't found a way to achieve this.

Any thoughts about this?

(It doesn't need to be an Ionic specific solution.)

Update (2017-10-13)

Based on Bobby's answer, I achieved what I wanted both when installing and uninstalling a plugin. I've created 4 hooks: after_plugin_add, after_plugin_rm, before_plugin_add, before_plugin_rm.

The before hooks copy the template (config.tpl.xml) into the config.xml file and the after hooks do the inverse.

The before_plugin_add and before_plugin_rm hooks are as follows:

#!/usr/bin/env node
var fs = require('fs');
var path = require('path');

var ROOT_DIR = process.argv[2];
var FILES = {
    SRC: 'config.tpl.xml',
    DEST: 'config.xml'
};

var srcFileFull = path.join(ROOT_DIR, FILES.SRC);
var destFileFull = path.join(ROOT_DIR, FILES.DEST);

var templateData = fs.readFileSync(srcFileFull, 'utf8');
fs.writeFileSync(destFileFull, templateData);

The after_plugin_add and after_plugin_rm hooks are almost identical, just swapping FILES.SRC and FILES.DEST values.

Lucas Basquerotto
  • 7,260
  • 2
  • 47
  • 61
  • Thanks for this! One note: This line ```var ROOT_DIR = process.argv[2];``` doesn't work in my case because process.argv[2] is resolved to "prepare". What works in my case is `var ROOT_DIR = '';`. I'm using Ionic 4 and Cordova CLI 9.0. – Klemens Zleptnig Feb 15 '20 at 13:11
  • Another question: Do you add config.xml to .gitignore? From my point of view it would make sense to not have it checked in. – Klemens Zleptnig Feb 15 '20 at 13:19
  • @KlemensZleptnig I add `config.yml` to `.gitignore`. I don't know how to get the root dir in an Ionic4 hook :/ In a cordova project the above should work, though. You use them as a cordova hook or ionic is handling it behind the scenes? I think Ionic4 is adding arguments. You could try to log them like `console.log('1', process.argv[1])`, `console.log('2', process.argv[2])`, `console.log('3', process.argv[3])`, and so on. – Lucas Basquerotto Feb 15 '20 at 18:26
  • I use them as Cordova hooks, just like you described it. Ionic calls Cordova, when I execute `$ ionic cordova prepare android --no-build` it will run `cordova prepare android`. These are the argv: `[ '~/.nvm/versions/v10.16.0/bin/node', '~/.nvm/versions/v10.16.0/bin/cordova', 'prepare', 'android' ]` when I do a `console.log(process.argv);`. But as I wrote, it works if I just use a relative path based on the project root. – Klemens Zleptnig Feb 17 '20 at 09:11
  • I'm having some issues with the plugin hooks (the prepare hook works great). I have a template string for the resource file ``, so Cordova complains with his error message: `Source path does not exist: ${ANDROID_GOOGLE_SERVICES_JSON_FILE}` because it seems to check the if the path exists. – Klemens Zleptnig Mar 26 '20 at 10:40
  • @KlemensZleptnig The variable `${ANDROID_GOOGLE_SERVICES_JSON_FILE}` should be only in the template file (`config.tpl.yml`) and not in the file `config.yml`. If `cordova` is complaining then maybe you have that variable in the generated file (`config.yml`). Give it a look and see if this is the case. – Lucas Basquerotto Mar 26 '20 at 11:55
  • 1
    But isn't the idea of exchanging the files that during installation of the plugin, `config.tpl.yml` becomes `config.xml` so that the new plugins get added to it, and after installation, this is reverted? According to that logic - if I understood it right - during installation, there will be variables in `config.xml`. – Klemens Zleptnig Mar 26 '20 at 13:33
  • 1
    @KlemensZleptnig This topic is a bit old so I forgot about that :P. It seems that depending on where you put the variable (in your case, it represents a path) cordova could give errors, so I don't know if you should use the hooks I mentioned. **What I recommend you to do** (and it's what I normally do), **is defining plugins directly in the file `config.tpl.yml`** instead of adding them from the command line, so that when the preparation step is done, the plugin is installed, and you shouldn't need hooks nor copying code from `config.xml` to the template. – Lucas Basquerotto Mar 26 '20 at 15:19
  • How did you keep the ${FACEBOOK_APP_ID} variables in tpl file on after_hooks? – Iraklis Bekiaris Aug 04 '21 at 07:33
  • @IraklisBekiaris I'm not using the above code for some time so I'm not sure about your problem, but **I think that your `before` hook is not being called**. If the `before` hook is called `config.yml` receives the contant of `config.tpl.yml`, then the plugin is installed, and after the installation the `after` hook is called, making the `config.tpl.yml` file receive the content of `config.yml` that has the new plugin, and `${FACEBOOK_APP_ID}` will remain the same, because the config file received the template content initially. **So make sure that the `before` hook is being called.** – Lucas Basquerotto Aug 04 '21 at 13:03

1 Answers1

5

One solution could be to create hooks for before_plugin_install and after_plugin_install.

On before_plugin_install copy the cordova.tpl.xml to cordova.xml.
... Plugin is installed ...
On after_plugin_install copy cordova.xml to cordova.tpl.xml
Bobby
  • 166
  • 1
  • 4
  • How did you keep the ${FACEBOOK_APP_ID} variables in tpl file on after_hooks? – Iraklis Bekiaris Aug 04 '21 at 07:57
  • ${FACEBOOK_APP_ID} is part of the template. So, you copy the template on top of the original file. Install the new plugin. This should not affect the ${FACEBOOK_APP_ID} variable. Then make this file a template again. – Bobby Aug 05 '21 at 08:12