Repository: ember-cli/ember-cli
Pull request #7606 (2018-02-05 17:18:57)

Introduce `emberAppUtils`
There are several motivations behind this change

+ reduct the API surface by passing parameters explicitly (as opposed to passing full `config` object)
+ make `ember-app` file smaller (this is going to happen regardless as a part of Default Packager work)
+ add documentation and tests to utility functions.

Turbo87: any reason for putting them all in one file instead of one util per file?
twokul: since they are all related to `ember-app` and are not used anywhere else, I decided to put them into one file @Turbo87 
twokul: @rwjblue I don't have a preference, what sounds better to you?

Patch:
diff --git a/lib/broccoli/ember-app.js b/lib/broccoli/ember-app.js
index 834e5bd21..5b4d624ce 100644
--- a/lib/broccoli/ember-app.js
+++ b/lib/broccoli/ember-app.js
@@ -12,7 +12,6 @@ const chalk = require('chalk');
 const resolve = require('resolve');
 
 const Project = require('../models/project');
-const cleanBaseURL = require('clean-base-url');
 const SilentError = require('silent-error');
 
 let preprocessJs = p.preprocessJs;
@@ -38,6 +37,7 @@ const isNull = require('ember-cli-lodash-subset').isNull;
 const Funnel = require('broccoli-funnel');
 const funnelReducer = require('broccoli-funnel-reducer');
 const logger = require('heimdalljs-logger')('ember-cli:ember-app');
+const emberAppUtils = require('../utilities/ember-app-utils');
 const addonProcessTree = require('../utilities/addon-process-tree');
 const lintAddonsByType = require('../utilities/lint-addons-by-type');
 const experiments = require('../experiments');
@@ -49,6 +49,10 @@ const strategies = require('./strategies');
 const createVendorJsStrategy = strategies.createVendorJsStrategy;
 const createApplicationJsStrategy = strategies.createApplicationJsStrategy;
 
+const normalizeUrl = emberAppUtils.normalizeUrl;
+const convertObjectToString = emberAppUtils.convertObjectToString;
+const calculateBaseTag = emberAppUtils.calculateBaseTag;
+
 let DEFAULT_CONFIG = {
   storeConfigInMeta: true,
   autoRun: true,
@@ -843,16 +847,22 @@ class EmberApp {
   _configReplacePatterns() {
     return [{
       match: /{{rootURL}}/g,
-      replacement: calculateRootURL,
+      replacement(config) {
+        return normalizeUrl(config.rootURL);
+      },
     }, {
       match: /{{EMBER_ENV}}/g,
-      replacement: calculateEmberENV,
+      replacement(config) {
+        return convertObjectToString(config.EmberENV);
+      },
     }, {
       match: /{{content-for ['"](.+)["']}}/g,
       replacement: this.contentFor.bind(this),
     }, {
       match: /{{MODULE_PREFIX}}/g,
-      replacement: calculateModulePrefix,
+      replacement(config) {
+        return config.modulePrefix;
+      },
     }];
   }
 
@@ -1856,7 +1866,7 @@ class EmberApp {
     // This normalizes `rootURL` to the value which we use everywhere inside of Ember CLI.
     // This makes sure that the user doesn't have to account for it in application code.
     if ('rootURL' in config) {
-      config.rootURL = calculateRootURL(config);
+      config.rootURL = normalizeUrl(config.rootURL);
     }
 
     switch (type) {
@@ -1894,7 +1904,7 @@ class EmberApp {
     @param {Object} config
   */
   _contentForHead(content, config) {
-    content.push(calculateBaseTag(config));
+    content.push(calculateBaseTag(config.baseURL, config.locationType));
 
     if (this.options.storeConfigInMeta) {
       content.push(`<meta name="${config.modulePrefix}/config/environment" content="${escape(JSON.stringify(config))}" />`);
@@ -1929,7 +1939,7 @@ class EmberApp {
       let shouldUseSrc = experiments.MODULE_UNIFICATION && !!this.trees.src;
       let moduleToRequire = `${config.modulePrefix}/${shouldUseSrc ? 'src/main' : 'app'}`;
       content.push('if (!runningTests) {');
-      content.push(`  require("${moduleToRequire}")["default"].create(${calculateAppConfig(config)});`);
+      content.push(`  require("${moduleToRequire}")["default"].create(${convertObjectToString(config.APP)});`);
       content.push('}');
     }
   }
@@ -1937,47 +1947,6 @@ class EmberApp {
 
 module.exports = EmberApp;
 
-/*
-  Returns the <base> tag for index.html
-
-  @param  {Object} config Application configuration
-  @return {String}        Base tag or empty string
- */
-function calculateBaseTag(config) {
-  let baseURL = cleanBaseURL(config.baseURL);
-  let locationType = config.locationType;
-
-  if (locationType === 'hash') {
-    return '';
-  }
-
-  if (baseURL) {
-    return `<base href="${baseURL}" />`;
-  } else {
-    return '';
-  }
-}
-
-function calculateRootURL(config) {
-  if (config.rootURL === '') {
-    return config.rootURL;
-  }
-
-  return cleanBaseURL(config.rootURL) || '';
-}
-
-function calculateEmberENV(config) {
-  return JSON.stringify(config.EmberENV || {});
-}
-
-function calculateAppConfig(config) {
-  return JSON.stringify(config.APP || {});
-}
-
-function calculateModulePrefix(config) {
-  return config.modulePrefix;
-}
-
 function addOutputFile(strategy, container, assetPath, options) {
   let outputFile = options.outputFile;
 
diff --git a/lib/utilities/ember-app-utils.js b/lib/utilities/ember-app-utils.js
new file mode 100644
index 000000000..c807c9346
--- /dev/null
+++ b/lib/utilities/ember-app-utils.js
@@ -0,0 +1,53 @@
+'use strict';
+
+const cleanBaseURL = require('clean-base-url');
+
+/**
+ * Returns a normalized url given a string.
+ * Returns an empty string if `null`, `undefined` or an empty string are passed
+ * in.
+ *
+ * @method normalizeUrl
+ * @param {String} Raw url.
+ * @return {String} Normalized url.
+*/
+function normalizeUrl(rootURL) {
+  if (rootURL === undefined || rootURL === null || rootURL === '') {
+    return '';
+  }
+
+  return cleanBaseURL(rootURL);
+}
+
+/**
+ * Converts Javascript Object to a string.
+ * Returns an empty object string representation if a "falsy" value is passed
+ * in.
+ *
+ * @method convertObjectToString
+ * @param {Object} Any Javascript Object.
+ * @return {String} A string representation of a Javascript Object.
+*/
+function convertObjectToString(env) {
+  return JSON.stringify(env || {});
+}
+
+/**
+ * Returns the <base> tag for index.html.
+ *
+ * @method calculateBaseTag
+ * @param {String} baseURL
+ * @param {String} locationType 'history', 'none' or 'hash'.
+ * @return {String} Base tag or an empty string
+*/
+function calculateBaseTag(baseURL, locationType) {
+  let normalizedBaseUrl = cleanBaseURL(baseURL);
+
+  if (locationType === 'hash') {
+    return '';
+  }
+
+  return normalizedBaseUrl ? `<base href="${normalizedBaseUrl}" />` : '';
+}
+
+module.exports = { normalizeUrl, convertObjectToString, calculateBaseTag };


Test patch:
diff --git a/tests/unit/utilities/ember-app-utils-test.js b/tests/unit/utilities/ember-app-utils-test.js
new file mode 100644
index 000000000..9af731abc
--- /dev/null
+++ b/tests/unit/utilities/ember-app-utils-test.js
@@ -0,0 +1,90 @@
+'use strict';
+
+const expect = require('chai').expect;
+
+const emberAppUtils = require('../../../lib/utilities/ember-app-utils');
+
+const normalizeUrl = emberAppUtils.normalizeUrl;
+const calculateBaseTag = emberAppUtils.calculateBaseTag;
+const convertObjectToString = emberAppUtils.convertObjectToString;
+
+describe('ember-app-utils', function() {
+  describe(`calculateBaseTag`, function() {
+    ['auto', 'history'].forEach(locationType => {
+      it(`generates a base tag correctly for location: ${locationType}`, function() {
+        expect(
+          calculateBaseTag('/', locationType),
+          `base tag was generated correctly`
+        ).to.equal('<base href="/" />');
+      });
+    });
+
+    it('returns an empty string if location is "hash"', function() {
+      expect(
+        calculateBaseTag('/', 'hash'),
+        `base tag was generated correctly`
+      ).to.equal('');
+    });
+
+    [null, undefined, ''].forEach(url => {
+      it(`returns an empty string if the url is ${url === '' ? 'empty string' : url}`, function() {
+        expect(calculateBaseTag(url, 'hash')).to.equal('');
+      });
+    });
+  });
+
+  describe(`convertObjectToString`, function() {
+    it('transforms config object into a string', function() {
+      expect(
+        convertObjectToString({ foobar: 'baz' }),
+        `config was transformed correctly`
+      ).to.equal('{"foobar":"baz"}');
+    });
+
+    it('returns empty object string for "falsy" values', function() {
+      let invalidValues = [null, undefined, 0];
+
+      invalidValues.forEach(value => {
+        expect(
+          convertObjectToString(value),
+          `${value} was transformed correctly`
+        ).to.equal('{}');
+      });
+    });
+  });
+
+  describe(`normalizeUrl`, function() {
+    it('transforms input values to valid urls', function() {
+      expect(
+        normalizeUrl('local/people'),
+        '`local/people` was transformed correctly'
+      ).to.equal('/local/people/');
+
+      expect(
+        normalizeUrl('people'),
+        '`people` was transformed correctly'
+      ).to.equal('/people/');
+
+      expect(
+        normalizeUrl('/people'),
+        '`/people` was transformed correctly'
+      ).to.equal('/people/');
+
+      expect(
+        normalizeUrl('/'),
+        '`/` is transformed correctly'
+      ).to.equal('/');
+    });
+
+    it('returns an empty string for `null`, `undefined` and empty string', function() {
+      let invalidUrls = [null, undefined, ''];
+
+      invalidUrls.forEach(url => {
+        expect(
+          normalizeUrl(url),
+          `${url} was transformed correctly`
+        ).to.equal('');
+      });
+    });
+  });
+});