Search code examples
javascriptgruntjsyamlgrunt-contrib-concat

Grunt: Access Sequence/List Data from External YAML File


I am trying to access values from an external YAML file in my Gruntfile using:

external = grunt.file.readYAML('_config.yml');

The _config.yml file has the following example data:

computer:
  parts:
    - name: brand1
      type: cpu
    - name: brand2
      type: gpu
    - name: brand3
      type: hd

I've been trying to access the multi-level YAML data using <%= %> grunt templating to get the different name and type values.

module.exports = {
  concat: {
    src: ['htdocs/<%= external.computer.parts['type'] %>/<%= external.computer.parts['name'] %>/*.js'],
    dest: 'htdocs/output.js'
  }
};

The main goal has been to concat files from different directories this way into one, but I can't seem to access data from the _config.yml file beyond external.computer.parts. FYI, the structure of the _config.yml file has to remained unchanged.

How do you access a sequence/list with different properties this way?


Solution

  • Below are a couple of solutions to consider. However, firstly let's understand what using grunt.file.readYAML() to parse your _config.yml file does. It essentially produces the following object:

    {
      computer: {
        parts: [
          {
            name: 'brand1',
            type: 'cpu'
          },
          {
            name: 'brand2',
            type: 'gpu'
          },
          {
            name: 'brand3',
            type: 'hd'
          }
        ]
      }
    }
    

    Note how the value of parts is an array of objects.


    Solution 1:

    Given that you want to utilize grunt templates (i.e. <%= %>) to obtain the different name and type values, consider configuring your concat task in your Gruntfile.js as follows:

    Gruntfile.js

    module.exports = function (grunt) {
    
      grunt.loadNpmTasks('grunt-contrib-concat');
    
      grunt.initConfig({
        external: grunt.file.readYAML('_config.yml'),
    
        concat: {
          dist: {
            options: {
              // ...
            },
            src: [
              'htdocs/<%= external.computer.parts[0].type %>/<%= external.computer.parts[0].name %>/*.js',
              'htdocs/<%= external.computer.parts[1].type %>/<%= external.computer.parts[1].name %>/*.js',
              'htdocs/<%= external.computer.parts[2].type %>/<%= external.computer.parts[2].name %>/*.js'
            ],
            dest: 'htdocs/output.js'
          }
        }
        // ...
      });
    
      grunt.registerTask('default', [ 'concat' ]);
    
    };
    
    

    Notes:

    1. The value of the external property of the object passed into the grunt.initConfig method is essentially the aforementioned object, i.e. it's the result of utiizing grunt.file.readYAML() to parse your _config.yml.

    2. The value of the src property of the dist target, (which is associated with the concat task), is an array. Each item of this array is where we utilize the <% ... %> notation to reference the parts from your .yml file.

      Note how we reference each object in the external.computer.parts array by it's index, i.e. [0], [1], [2]

      'htdocs/<%= external.computer.parts[0].type %>/<%= external.computer.parts[0].name %>/*.js'
                                          ^                                      ^
      

    Solution 2:

    Another way to achieve your requirement is to not utilize grunt templates, i.e. <% ... %>, at all. Consider the following solution:

    Gruntfile.js

    module.exports = function (grunt) {
    
      grunt.loadNpmTasks('grunt-contrib-concat');
    
      var external = grunt.file.readYAML('_config.yml');
    
      grunt.initConfig({
        concat: {
          dist: {
            options: {
              // ...
            },
            src: external.computer.parts.map(function(part) {
              return 'htdocs/' + part.type + '/' + part.name + '/*.js'
            }),
            dest: 'htdocs/output.js'
          }
        }
        // ...
      });
    
      grunt.registerTask('default', [ 'concat' ]);
    
    };
    

    Notes:

    1. This time we assign the result of parsing your _config.yml file to a variable named external:

      var external = grunt.file.readYAML('_config.yml');
      
    2. The value of the src property is computed by utilizing the map() method. Here we create a new array of glob patterns.

      src: external.computer.parts.map(function(part) {
        return 'htdocs/' + part.type + '/' + part.name + '/*.js'
      }),
      

    Benefits:

    One of the key benefits that Solution 2 has over Solution 1 is:

    If we need to add a new part (name and tyoe) to _config.yml. For example:

    computer:
      parts:
        - name: brand1
          type: cpu
        - name: brand2
          type: gpu
        - name: brand3
          type: hd
        - name: brand4      <-------
          type: foo         <-------
    

    With Solution 1 we will need to add it to the src configuration in the Gruntfile.js. For example:

    src: [
      'htdocs/<%= external.computer.parts[0].type %>/<%= external.computer.parts[0].name %>/*.js',
      'htdocs/<%= external.computer.parts[1].type %>/<%= external.computer.parts[1].name %>/*.js',
      'htdocs/<%= external.computer.parts[2].type %>/<%= external.computer.parts[2].name %>/*.js',
    
       // Newly added...
      'htdocs/<%= external.computer.parts[3].type %>/<%= external.computer.parts[3].name %>/*.js'
    ],
    

    With Solution 2 we don't have to change the src configuration in the Gruntfile.js at all.


    Edit:

    If you're using a fairly recent version of node.js then you can also refactor Solution 2 as follows:

    Gruntfile.js

    module.exports = function (grunt) {
    
      grunt.loadNpmTasks('grunt-contrib-concat');
    
      const { computer: { parts } } = grunt.file.readYAML('_config.yml');
    
      grunt.initConfig({
        concat: {
          dist: {
            options: {
              // ...
            },
            src: parts.map(({ type, name }) => `htdocs/${type}/${name}/*.js`),
            dest: 'htdocs/output.js'
          }
        }
        // ...
      });
    
      grunt.registerTask('default', [ 'concat' ]);
    
    };
    

    Please ignore StackOverflow's inability to syntax highlight the above example correctly.

    Notes:

    This refactored version utilizes some ES6 features as follows:

    • Object destructuring is used to unpack the parts property/value from the parsed _config.yml, into a parts variable:

      var { computer: { parts } } = grunt.file.readYAML('_config.yml');
      
    • The value of the src property is computed using an Arrow function with the map() method, and Template Literals are used instead of the plus operator (+) for string concatenation.

      src: parts.map(({ type, name }) => `htdocs/${type}/${name}/*.js`),