BBaysinger
BBaysinger

Reputation: 6987

Grunt task to assemble JSON files from filenames in JSON

I have a JSON file that looks like:

{
    "someRandomStuff": "myRandomStuff",
    "includeNodesFromFiles": {
        "item1": "item1.json",
        "item2": "item2.json",
        "item3": "item3.json"
    }
}

And now I want to replace item1, item2, and item3 with JSON content from each respective file, so the destination file would look like:

{
    "someRandomStuff": "myRandomStuff",
    "includeNodesFromFiles": {
        "item1": {"whatever": "...whatever was in item1.json"},
        "item2": {"whatever": "...whatever was in item2.json"},
        "item3": {"whatever": "...whatever was in item3.json"},
    }
}

Or similarly for an array:

{
    "someRandomStuff": "myRandomStuff",
    "includeNodesFromFiles": [
        "item1.json",
        "item2.json",
        "item3.json"
    ]
}

To:

{
    "someRandomStuff": "myRandomStuff",
    "includeNodesFromFiles": [
        {"whatever": "...whatever was in item1.json"},
        {"whatever": "...whatever was in item2.json"},
        {"whatever": "...whatever was in item3.json"}
    ]
}

How could I do that with Grunt? I'm not finding a Grunt task that will do that out-of-the-box so far.

New to Grunt, so please bear with me.

Upvotes: 0

Views: 674

Answers (3)

BBaysinger
BBaysinger

Reputation: 6987

This is the solution I came up with. It recursively replaces filenames with files, if they exist. Also accepts a base URL for the files to be included:

grunt.registerTask('buildJson', function(name) {

    let config = grunt.config.get('buildJson'); // Get the config options from gruntInitConfig for our task.
    let options = name ? (config[name] || config) : config; // If there isn't a config option that matches, use the object itself

    function genJson(path, baseUrl = '') {

      function checkIfFile(fPath) {
        try {
          if (fs.lstatSync(fPath).isFile()) {
            return true;
          } else {
            return false;
          }
        } catch (e) {
          if (e.code == 'ENOENT') {
            return false;
          } else if (e.code == 'ENAMETOOLONG') {
            return false;
          } else {
            console.error(e);
          }
        }
      }

      var json = JSON.parse(fs.readFileSync(path, {
        encoding: 'utf8'
      }));

      return JSON.stringify(json, function(key, value) {
        if (checkIfFile(baseUrl + value)) {
          return JSON.parse(genJson(baseUrl + value));
        } else {
          return value;
        }
      }, 2);
    }

    let files = grunt.file.expand(options.srcFiles);

    for (let file in files) {

      let srcPath = files[file];
      // Determine the output path to write our merged json file.
      let outputPath = path.join(options.destDir, path.basename(srcPath));
      let destJson = genJson(srcPath, options.baseUrl);

      grunt.file.write(outputPath, destJson); // Write to disk.

    }

  });

Upvotes: 0

RobC
RobC

Reputation: 24962

Short answer: This is a very custom requirement and there are no existing grunt plugins which will achieve this that I'm aware of.


Solution:

You'll need to create your own grunt plugin to handle this type of requirement. The following steps describe how this can be achieved:

  1. Firstly create a plugin file as follows. Lets name the file json-replace.js:

    json-replace.js

    /**
     * Custom grunt plugin replaces JSON values (filepatha) with JSON file content.
     */
    module.exports = function(grunt) {
    
      'use strict';
    
      var path = require('path');
    
      /**
       * Custom grunt multi task to replace values in JSON.
       */
      grunt.registerMultiTask('jsonReplace', 'Replace values in JSON', function() {
    
        // Read the configuration values.
        var src = this.data.src;
        var dest = this.data.dest;
        var keyName = this.data.key;
    
        var baseDir = path.dirname(src);
    
        // Default options
        var opts = this.options({
          indent: 2
        });
    
        /**
         * Determines whether the passed value is an Array.
         * @param {*} value - A reference to the value to check.
         * @returns {Boolean} - true if the value is an Array, otherwise false.
         */
        function isArray(value) {
          return Array.isArray(value);
        }
    
        /**
         * Determines whether the passed value is an Object.
         * @param {*} value - A reference to the value to check.
         * @returns {Boolean} - true if the value is an Object, otherwise false.
         */
        function isObject(value) {
          return Object.prototype.toString.call(value) === '[object Object]';
        }
    
        /**
         * Reads a file's contents, parsing the data as JSON.
         * @param {String} srcPath - The filepath to the JSON file to parse.
         * @returns {Object}- The parsed JSON data.
         */
        function readJson(srcPath) {
          return grunt.file.readJSON(srcPath);
        }
    
        /**
         * Writes JSON data to a file.
         * @param {String} destPath - A filepath for where to save the file.
         * @param {Object|Array} data - Value to covert to JSON and saved to file.
         * @param {Number} [indent=2] - The no. of spaces to indent the JSON.
         */
        function writeJson(destPath, data, indent) {
          indent = (typeof indent !== 'undefined') ? indent : 2;
          grunt.file.write(destPath, JSON.stringify(data, null, indent));
          grunt.log.writeln('Saved \x1b[96m1\x1b[0m file');
        }
    
        /**
         * Checks whether a file exists and logs any missing files to console.
         * @param {String} filePath - The filepath to check for its existence.
         * @returns {Boolean} - true if the filepath exists, otherwise false.
         */
        function fileExists(filePath) {
          if (!grunt.file.exists(filePath)) {
            grunt.fail.warn('Unable to read \"' + filePath + '\"');
            return false;
          }
          return true;
        }
    
        /**
         * Checks whether type of value is a string and logs an error if not.
         * @param {*} value - The value to check
         * @returns {Boolean} - true if type of value is 'string', otherwise false.
         */
        function isString(value) {
          if (typeof value !== 'string') {
            grunt.fail.warn('Value type must be a string: found \"' + value + '\"');
            return false;
          }
          return true;
        }
    
        /**
         * Processes each Array item for a given key.
         * @param {Object} data - The parsed JSON data to process.
         * @param {String} keyName - Name of key whose Array values to process.
         * @param {String} baseDir - Base directory path of the source json file.
         */
        function processArrayItems(data, keyName, baseDir) {
          var replacement = [];
    
          data[keyName].forEach(function(item) {
            var fullPath = path.join(baseDir, item);
    
            if (isString(item) && fileExists(fullPath)) {
              replacement.push(readJson(fullPath));
            }
          });
          data[keyName] = replacement;
          writeJson(dest, data, opts.indent);
        }
    
        /**
         * Processes an Objects key/value pair for a given Object.
         * @param {Object} data - The parsed JSON data to process.
         * @param {String} keyName - Name of key whose property values to process.
         * @param {String} baseDir - Base directory path of the source json file.
         */
        function processObjectValues(data, keyName, baseDir) {
          var replacement = {};
    
          Object.keys(data[keyName]).forEach(function(key) {
            var accessor = data[keyName][key];
            var fullPath = path.join(baseDir, accessor);
    
            if (isString(accessor) && fileExists(fullPath)) {
              replacement[key] = readJson(fullPath);
            }
          });
    
          data[keyName] = replacement;
          writeJson(dest, data, opts.indent);
        }
    
        // Read the source JSON file
        var srcData = readJson(src);
    
        // Check if the `key` provided exists in source JSON.
        if (!srcData[keyName]) {
          grunt.fail.warn('Missing given key "' + keyName + '" in ' + src);
        }
    
        // Invoke the appropriate processing for key value.
        if (isArray(srcData[keyName])) {
          processArrayItems(srcData, keyName, baseDir);
        } else if (isObject(srcData[keyName])) {
          processObjectValues(srcData, keyName, baseDir);
        } else {
          grunt.fail.warn('Value for "' + keyName + '" must be object or array');
        }
    
      });
    };
    
  2. Save json-replace.js in a folder named custom-grunt-tasks which resides in your projects root directory (i.e. at as the same level as Gruntfile.js and package.json). For instance:

    .
    ├── Gruntfile.js
    ├── custom-grunt-tasks    <---
    │   └── json-replace.js   <---
    ├── node_modules
    │   └── ...
    ├── package.json
    └── ...
    
  3. Add the following Task to your Gruntfile.js:

    Gruntfile.js

    module.exports = function(grunt) {
    
      grunt.loadTasks('custom-grunt-tasks');
    
      grunt.initConfig({
        jsonReplace: {    // <-- Task
          targetA: {      // <-- Target
            src: 'path/to/source.json',
            dest: 'path/to/output/file.json',
            key: 'includeNodesFromFiles'
          }
        }
        // ...
      });
    
      grunt.registerTask('default', ['jsonReplace']);
    }
    

    Notes: (regarding configuration of Gruntfile.js above)

    • The line which reads grunt.loadTasks('custom-grunt-tasks'); loads the custom plugin (i.e. json-replace.js) from the directory named custom-grunt-tasks.

    • A Task named jsonReplace is added to grunt.initConfig({...}) which contains one Target arbitrarily named targetA.

    • The value for the src property should be replaced with a valid filepath which points to your source .json file.

    • The value for the dest property should be replaced with a filepath for where the new .json file should be saved.

    • The value for the key should be replaced with a valid keyname. The name of the key provided should be for the key which holds either an Object or Array of .json filepaths (For example; includeNodesFromFiles, as given in your two examples)


Additional info:

  1. json-replace.js is multi-taskable which basically means you can configure multiple Targets inside the jsonReplace task if required. For example:

    // ...
    jsonReplace: {    // <-- Task
      targetA: {      // <-- Target
        src: 'path/to/source.json',
        dest: 'path/to/output/file.json',
        key: 'includeNodesFromFiles'
      },
      targetB: {      // <-- Another Target
        src: 'path/to/another/source.json',
        dest: 'path/to/output/another/file.json',
        key: 'anotherKeyName'
      }
    }
    // ...
    

    Multiple Targets may be useful if you want to process multiple .json files.

  2. json-replace.js expects the value of the key (e.g. includeNodesFromFiles) to be either:

    • An Object with nested key/value pairs, (as per your first example), whereby each nested key has a filepath value for an existing .json file.
    • Or, an Array, (as per your second example), whereby each item of the Array is a filepath value for an existing .json file.

    • Note: An error will be logged to the console if the structure of the given JSON key does match either of the two aforementioned structures.

  3. json-replace.js indents the content of the resultant .json file(s) using two spaces by default. However, if you want to change this you can utilize the indent option. For example the following Task configuration will indent the resultant .json file by four spaces:

    // ...
    jsonReplace: {
      options: {
        indent: 4  // <-- Custom indent option
      },
      targetA: {
        src: 'path/to/source.json',
        dest: 'path/to/output/file.json',
        key: 'includeNodesFromFiles'
      }
    }
    // ... 
    

Important: When defining the filepath values in the source .json file (e.g. item1.json, item2.json etc) this solution expects them to be relative to the source .json file itself.

Upvotes: 2

Max Sinev
Max Sinev

Reputation: 6034

This is pretty simple task with just loading file and change value in object(all file paths are relative to gruntfile.js):

    grunt.registerTask('mytask', 'My super task', () => {
        // file with json with file paths
        //{"someRandomStuff":"myRandomStuff","includeNodesFromFiles":{"item1":"item1.json","item2":"item2.json","item3":"item3.json"}}
        let main = JSON.parse(fs.readFileSync('myjson.json', 'utf8'));
        Object.keys(main.includeNodesFromFiles).forEach((key) => {
            main.includeNodesFromFiles[key] = JSON.parse(fs.readFileSync(main.includeNodesFromFiles[key], 'utf8'));
        });
        //... do some stuff
        grunt.log.writeln(JSON.stringify(main)); //{"someRandomStuff":"myRandomStuff","includeNodesFromFiles":{"item1":{},"item2":{},"item3":{}}}
});

Upvotes: 0

Related Questions