Repository: ember-cli/ember-cli
Pull request #7147 (2017-06-25 22:38:27)

Add Replacer to FileInfo
This enables a blueprint to augment the output of a given file provided.

A concrete example would be a package.json replacer.

```js
function replacer(content) {
  let pkg = JSON.parse(content);

  delete pkg.devDependencies['ember-resolver'];
  delete pkg.devDependencies['ember-ajax'];

  pkg.devDependencies['ember-strict-resolver'] = '^0.0.3';
  pkg.devDependencies['ember-native-dom-helpers'] = '^0.4.1';
  pkg.devDependencies['ember-native-dom-event-dispatcher'] = '^0.6.1';
  pkg.devDependencies['ember-fetch'] = '^2.1.0';

  return JSON.stringify(pkg, null, 2);
}


let options = { 
  /* other valid options */,
  replacer
};

let info = new FileInfo(options);
```


Patch:
diff --git a/blueprints/addon/index.js b/blueprints/addon/index.js
index 02e118377..4972dc911 100644
--- a/blueprints/addon/index.js
+++ b/blueprints/addon/index.js
@@ -1,20 +1,27 @@
 'use strict';
 
 const fs = require('fs-extra');
-const existsSync = require('exists-sync');
 const path = require('path');
 const walkSync = require('walk-sync');
 const stringUtil = require('ember-cli-string-utils');
 const uniq = require('ember-cli-lodash-subset').uniq;
 const SilentError = require('silent-error');
 const sortPackageJson = require('sort-package-json');
+
 let date = new Date();
 
 const normalizeEntityName = require('ember-cli-normalize-entity-name');
-const stringifyAndNormalize = require('../../lib/utilities/stringify-and-normalize');
+const FileInfo = require('../../lib/models/file-info');
+
+const replacers = {
+  'package.json'(content) {
+    return this.updatePackgeJson(content);
+  },
+};
 
+const description = 'The default blueprint for ember-cli addons.';
 module.exports = {
-  description: 'The default blueprint for ember-cli addons.',
+  description,
 
   filesToRemove: [
     'tests/dummy/app/styles/.gitkeep',
@@ -25,12 +32,12 @@ module.exports = {
     'testem.json',
   ],
 
-  generatePackageJson() {
-    let contents = this._readContentsFromFile('package.json');
+  updatePackgeJson(content) {
+    let contents = JSON.parse(content);
 
+    contents.name = this.locals(this.options).addonName;
+    contents.description = description;
     delete contents.private;
-    contents.name = '<%= addonName %>';
-    contents.description = this.description;
     contents.scripts = contents.scripts || {};
     contents.keywords = contents.keywords || [];
     contents.dependencies = contents.dependencies || {};
@@ -60,7 +67,26 @@ module.exports = {
     contents['ember-addon'] = contents['ember-addon'] || {};
     contents['ember-addon'].configPath = 'tests/dummy/config';
 
-    this._writeContentsToFile(sortPackageJson(contents), 'package.json');
+    return JSON.stringify(sortPackageJson(contents), null, 2);
+  },
+
+  buildFileInfo(intoDir, templateVariables, file) {
+    let mappedPath = this.mapFile(file, templateVariables);
+    let options = {
+      action: 'write',
+      outputBasePath: path.normalize(intoDir),
+      outputPath: path.join(intoDir, mappedPath),
+      displayPath: path.normalize(mappedPath),
+      inputPath: this.srcPath(file),
+      templateVariables,
+      ui: this.ui,
+    };
+
+    if (file in replacers) {
+      options.replacer = replacers[file].bind(this);
+    }
+
+    return new FileInfo(options);
   },
 
   afterInstall() {
@@ -98,16 +124,10 @@ module.exports = {
   },
 
   files() {
-    if (this._files) { return this._files; }
-
-    this._appBlueprint = this.lookupBlueprint('app');
-    let appFiles = this._appBlueprint.files();
-
-    this.generatePackageJson();
-
+    let appFiles = this.lookupBlueprint('app').files();
     let addonFiles = walkSync(path.join(this.path, 'files'));
 
-    return this._files = uniq(appFiles.concat(addonFiles));
+    return uniq(appFiles.concat(addonFiles));
   },
 
   mapFile() {
@@ -137,15 +157,6 @@ module.exports = {
     return path;
   },
 
-  srcPath(file) {
-    let filePath = path.resolve(this.path, 'files', file);
-    if (existsSync(filePath)) {
-      return filePath;
-    } else {
-      return path.resolve(this._appBlueprint.path, 'files', file);
-    }
-  },
-
   normalizeEntityName(entityName) {
     entityName = normalizeEntityName(entityName);
 
@@ -156,21 +167,9 @@ module.exports = {
     return entityName;
   },
 
-  _readContentsFromFile(fileName) {
-    let packagePath = path.join(this._appBlueprint.path, 'files', fileName);
-    return this._readJsonSync(packagePath);
-  },
-
-  _writeContentsToFile(contents, fileName) {
-    let packagePath = path.join(this.path, 'files', fileName);
-    this._writeFileSync(packagePath, stringifyAndNormalize(contents));
-  },
-
-  _readJsonSync(path) {
-    return fs.readJsonSync(path);
-  },
-
-  _writeFileSync(path, content) {
-    fs.writeFileSync(path, content);
+  srcPath(file) {
+    let path = `${this.path}/files/${file}`;
+    let superPath = `${this.lookupBlueprint('app').path}/files/${file}`;
+    return fs.existsSync(path) ? path : superPath;
   },
 };
diff --git a/lib/models/file-info.js b/lib/models/file-info.js
index 8063ffc90..d8598d488 100644
--- a/lib/models/file-info.js
+++ b/lib/models/file-info.js
@@ -26,6 +26,7 @@ function diffHighlight(line) {
   }
 }
 
+const NOOP = _ => _;
 class FileInfo {
   constructor(options) {
     this.action = options.action;
@@ -34,6 +35,7 @@ class FileInfo {
     this.displayPath = options.displayPath;
     this.inputPath = options.inputPath;
     this.templateVariables = options.templateVariables;
+    this.replacer = options.replacer || NOOP;
     this.ui = options.ui;
   }
 
@@ -92,27 +94,33 @@ class FileInfo {
   }
 
   render() {
-    let path = this.inputPath,
-        context = this.templateVariables;
     if (!this.rendered) {
-      this.rendered = readFile(path)
-        .then(content => lstat(path)
-          .then(fileStat => {
-            if (isBinaryFile(content, fileStat.size)) {
-              return content;
-            } else {
-              try {
-                return processTemplate(content.toString(), context);
-              } catch (err) {
-                err.message += ` (Error in blueprint template: ${path})`;
-                throw err;
-              }
-            }
-          }));
+      this.rendered = this._render().then(result => this.replacer(result, this));
     }
+
     return this.rendered;
   }
 
+  _render() {
+    let path = this.inputPath;
+    let context = this.templateVariables;
+
+    return readFile(path)
+      .then(content => lstat(path)
+        .then(fileStat => {
+          if (isBinaryFile(content, fileStat.size)) {
+            return content;
+          } else {
+            try {
+              return processTemplate(content.toString(), context);
+            } catch (err) {
+              err.message += ` (Error in blueprint template: ${path})`;
+              throw err;
+            }
+          }
+        }));
+  }
+
   checkForConflict() {
     return new Promise((resolve, reject) => {
       fs.exists(this.outputPath, (doesExist, error) => {


Test patch:
diff --git a/tests/acceptance/addon-smoke-test-slow.js b/tests/acceptance/addon-smoke-test-slow.js
index 56799a1e3..04719d516 100644
--- a/tests/acceptance/addon-smoke-test-slow.js
+++ b/tests/acceptance/addon-smoke-test-slow.js
@@ -148,7 +148,11 @@ function npmPack() {
 function tar() {
   return new Promise((resolve, reject) => {
     let output;
-    let tar = spawn('tar', ['-tf', `${addonName}-0.0.0.tgz`]);
+    let fileName = `${addonName}-0.0.0.tgz`;
+    if (fs.existsSync(fileName) === false) {
+      throw new Error(`unknown file: '${path.resolve(fileName)}'`);
+    }
+    let tar = spawn('tar', ['-tf', fileName]);
     tar.on('error', reject);
     tar.stdout.on('data', data => (output = data.toString()));
     tar.on('close', () => resolve(output));
diff --git a/tests/unit/blueprints/addon-test.js b/tests/unit/blueprints/addon-test.js
index b4b0a9a03..dbcb8510f 100644
--- a/tests/unit/blueprints/addon-test.js
+++ b/tests/unit/blueprints/addon-test.js
@@ -3,8 +3,6 @@
 const Blueprint = require('../../../lib/models/blueprint');
 const MockProject = require('../../helpers/mock-project');
 const expect = require('chai').expect;
-const path = require('path');
-const td = require('testdouble');
 
 describe('blueprint - addon', function() {
   describe('Blueprint.lookup', function() {
@@ -49,39 +47,24 @@ describe('blueprint - addon', function() {
 
   describe('direct blueprint require', function() {
     let blueprint;
-    let readJsonSync;
-    let writeFileSync;
-
     beforeEach(function() {
       blueprint = require('../../../blueprints/addon');
+      blueprint.options = {
+        entity: { name: 'my-cool-addon' },
+      };
       blueprint._appBlueprint = {
         path: 'test-app-blueprint-path',
       };
       blueprint.path = 'test-blueprint-path';
-
-      readJsonSync = td.replace(blueprint, '_readJsonSync');
-      td.when(readJsonSync(), { ignoreExtraArgs: true }).thenReturn({});
-
-      writeFileSync = td.replace(blueprint, '_writeFileSync');
-    });
-
-    afterEach(function() {
-      td.reset();
     });
 
     describe('generatePackageJson', function() {
       it('works', function() {
-        blueprint.generatePackageJson();
-
-        let captor = td.matchers.captor();
-
-        td.verify(readJsonSync(path.normalize('test-app-blueprint-path/files/package.json')));
-        td.verify(writeFileSync(path.normalize('test-blueprint-path/files/package.json'), captor.capture()));
-
+        let output = blueprint.updatePackgeJson(JSON.stringify({}));
         // string to test ordering
-        expect(captor.value).to.equal('\
+        expect(output).to.equal('\
 {\n\
-  "name": "<%= addonName %>",\n\
+  "name": "my-cool-addon",\n\
   "description": "The default blueprint for ember-cli addons.",\n\
   "keywords": [\n\
     "ember-addon"\n\
@@ -96,72 +79,35 @@ describe('blueprint - addon', function() {
   "ember-addon": {\n\
     "configPath": "tests/dummy/config"\n\
   }\n\
-}\n');
+}');
       });
 
       it('removes the `private` property', function() {
-        td.when(readJsonSync(), { ignoreExtraArgs: true }).thenReturn({
-          private: true,
-        });
-
-        blueprint.generatePackageJson();
-
-        let captor = td.matchers.captor();
+        let output = blueprint.updatePackgeJson(JSON.stringify({}));
 
-        td.verify(readJsonSync(path.normalize('test-app-blueprint-path/files/package.json')));
-        td.verify(writeFileSync(path.normalize('test-blueprint-path/files/package.json'), captor.capture()));
-
-        let json = JSON.parse(captor.value);
-        expect(json.private).to.be.undefined;
+        expect(JSON.parse(output).private).to.be.undefined;
       });
 
       it('overwrites `name`', function() {
-        td.when(readJsonSync(), { ignoreExtraArgs: true }).thenReturn({
-          name: 'test-name',
-        });
-
-        blueprint.generatePackageJson();
-
-        let captor = td.matchers.captor();
-
-        td.verify(readJsonSync(path.normalize('test-app-blueprint-path/files/package.json')));
-        td.verify(writeFileSync(path.normalize('test-blueprint-path/files/package.json'), captor.capture()));
-
-        let json = JSON.parse(captor.value);
-        expect(json.name).to.equal('<%= addonName %>');
+        let output = blueprint.updatePackgeJson(JSON.stringify({ name: 'OMG' }));
+        expect(JSON.parse(output).name).to.eql('my-cool-addon');
       });
 
       it('overwrites `description`', function() {
-        td.when(readJsonSync(), { ignoreExtraArgs: true }).thenReturn({
-          description: 'test-description',
-        });
-
-        blueprint.generatePackageJson();
-
-        let captor = td.matchers.captor();
+        let output = blueprint.updatePackgeJson(JSON.stringify({ description: 'OMG' }));
+        let json = JSON.parse(output);
 
-        td.verify(readJsonSync(path.normalize('test-app-blueprint-path/files/package.json')));
-        td.verify(writeFileSync(path.normalize('test-blueprint-path/files/package.json'), captor.capture()));
-
-        let json = JSON.parse(captor.value);
         expect(json.description).to.equal('The default blueprint for ember-cli addons.');
       });
 
       it('moves `ember-cli-babel` from devDependencies to dependencies', function() {
-        td.when(readJsonSync(), { ignoreExtraArgs: true }).thenReturn({
+        let output = blueprint.updatePackgeJson(JSON.stringify({
           devDependencies: {
             'ember-cli-babel': '1.0.0',
           },
-        });
-
-        blueprint.generatePackageJson();
+        }));
 
-        let captor = td.matchers.captor();
-
-        td.verify(readJsonSync(path.normalize('test-app-blueprint-path/files/package.json')));
-        td.verify(writeFileSync(path.normalize('test-blueprint-path/files/package.json'), captor.capture()));
-
-        let json = JSON.parse(captor.value);
+        let json = JSON.parse(output);
         expect(json.dependencies).to.deep.equal({
           'ember-cli-babel': '1.0.0',
         });
@@ -169,77 +115,48 @@ describe('blueprint - addon', function() {
       });
 
       it('does not push multiple `ember-addon` keywords', function() {
-        td.when(readJsonSync(), { ignoreExtraArgs: true }).thenReturn({
+        let output = blueprint.updatePackgeJson(JSON.stringify({
           keywords: ['ember-addon'],
-        });
-
-        blueprint.generatePackageJson();
-
-        let captor = td.matchers.captor();
-
-        td.verify(readJsonSync(path.normalize('test-app-blueprint-path/files/package.json')));
-        td.verify(writeFileSync(path.normalize('test-blueprint-path/files/package.json'), captor.capture()));
-
-        let json = JSON.parse(captor.value);
+        }));
+        let json = JSON.parse(output);
         expect(json.keywords).to.deep.equal(['ember-addon']);
       });
 
       it('overwrites any version of `ember-disable-prototype-extensions`', function() {
-        td.when(readJsonSync(), { ignoreExtraArgs: true }).thenReturn({
+        let output = blueprint.updatePackgeJson(JSON.stringify({
           devDependencies: {
             'ember-disable-prototype-extensions': '0.0.1',
           },
-        });
-
-        blueprint.generatePackageJson();
+        }));
 
-        let captor = td.matchers.captor();
-
-        td.verify(readJsonSync(path.normalize('test-app-blueprint-path/files/package.json')));
-        td.verify(writeFileSync(path.normalize('test-blueprint-path/files/package.json'), captor.capture()));
-
-        let json = JSON.parse(captor.value);
+        let json = JSON.parse(output);
         expect(json.devDependencies['ember-disable-prototype-extensions']).to.equal('^1.1.0');
       });
 
       it('overwrites `scripts.test`', function() {
-        td.when(readJsonSync(), { ignoreExtraArgs: true }).thenReturn({
+        let output = blueprint.updatePackgeJson(JSON.stringify({
           scripts: {
             test: 'test-string',
           },
-        });
+        }));
 
-        blueprint.generatePackageJson();
-
-        let captor = td.matchers.captor();
-
-        td.verify(readJsonSync(path.normalize('test-app-blueprint-path/files/package.json')));
-        td.verify(writeFileSync(path.normalize('test-blueprint-path/files/package.json'), captor.capture()));
-
-        let json = JSON.parse(captor.value);
+        let json = JSON.parse(output);
         expect(json.scripts.test).to.equal('ember try:each');
       });
 
       it('overwrites `ember-addon.configPath`', function() {
-        td.when(readJsonSync(), { ignoreExtraArgs: true }).thenReturn({
+        let output = blueprint.updatePackgeJson(JSON.stringify({
           'ember-addon': {
             configPath: 'test-path',
           },
-        });
-
-        blueprint.generatePackageJson();
-
-        let captor = td.matchers.captor();
+        }));
 
-        td.verify(readJsonSync(path.normalize('test-app-blueprint-path/files/package.json')));
-        td.verify(writeFileSync(path.normalize('test-blueprint-path/files/package.json'), captor.capture()));
-
-        let json = JSON.parse(captor.value);
+        let json = JSON.parse(output);
         expect(json['ember-addon'].configPath).to.equal('tests/dummy/config');
       });
 
       it('preserves dependency ordering', function() {
-        td.when(readJsonSync(), { ignoreExtraArgs: true }).thenReturn({
+        let output = blueprint.updatePackgeJson(JSON.stringify({
           dependencies: {
             b: '1',
             a: '1',
@@ -248,16 +165,9 @@ describe('blueprint - addon', function() {
             b: '1',
             a: '1',
           },
-        });
-
-        blueprint.generatePackageJson();
-
-        let captor = td.matchers.captor();
-
-        td.verify(readJsonSync(path.normalize('test-app-blueprint-path/files/package.json')));
-        td.verify(writeFileSync(path.normalize('test-blueprint-path/files/package.json'), captor.capture()));
+        }));
 
-        let json = JSON.parse(captor.value);
+        let json = JSON.parse(output);
         delete json.devDependencies['ember-disable-prototype-extensions'];
         expect(json.dependencies).to.deep.equal({ a: "1", b: "1" });
         expect(json.devDependencies).to.deep.equal({ a: "1", b: "1" });
diff --git a/tests/unit/models/file-info-test.js b/tests/unit/models/file-info-test.js
index 0b5dfe264..ac0d575c9 100644
--- a/tests/unit/models/file-info-test.js
+++ b/tests/unit/models/file-info-test.js
@@ -70,6 +70,25 @@ describe('Unit - FileInfo', function() {
     });
   });
 
+  it('allows mutation to the rendered file', function() {
+    validOptions.templateVariables.friend = 'Billy';
+    let fileInfo;
+
+    validOptions.replacer = function(content, theFileInfo) {
+      expect(theFileInfo).to.eql(fileInfo);
+      expect(content).to.eql('Howdy Billy\n');
+
+      return content.toUpperCase();
+    };
+
+    fileInfo = new FileInfo(validOptions);
+
+    return fileInfo.render().then(function(output) {
+      expect(output.trim()).to.equal('HOWDY BILLY',
+        'expects the template to have been run');
+    });
+  });
+
   it('rejects if templating throws', function() {
     let templateWithUndefinedVariable = path.resolve(__dirname,
       '../../fixtures/blueprints/with-templating/files/with-undefined-variable.txt');