Repository: tclindner/npm-package-json-lint (ts)

Issue:
Support for entires order?
I wonder if there is support for entries ordering such as: First `name` then `description` etc..

I would like it to have default ordering and if I want I can reorder it by given an array of entries.

Pull request description:
Closes #50 - Add support for checking package.json property order
Add a new rule, prefer-property-order, that allows users to specify an
array of properties in the order they desire.

Fixed a bug that caused alphabetical-sort to quietly go out of bounds

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4046ee4..a8bb73b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
 
 ### Removed
 
+## [2.8.0] - 2017-08-16
+### Added
+- New rule: [prefer-property-order](https://github.com/tclindner/npm-package-json-lint/wiki/prefer-property-order)
+
 ## [2.7.1] - 2017-08-15
 ### Fixed
 - [Issue #48](https://github.com/tclindner/npm-package-json-lint/issues/48) [valid-values-license](https://github.com/tclindner/npm-package-json-lint/wiki/valid-values-license)
diff --git a/package.json b/package.json
index edf8e62..4ca8c77 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "npm-package-json-lint",
-  "version": "2.7.1",
+  "version": "2.8.0",
   "description": "CLI app for linting package.json files.",
   "keywords": [
     "lint",
diff --git a/src/Config.js b/src/Config.js
index e28e07a..b6c9bb7 100755
--- a/src/Config.js
+++ b/src/Config.js
@@ -23,7 +23,8 @@ class Config {
       'no-restricted-dependencies',
       'no-restricted-devDependencies',
       'no-restricted-pre-release-dependencies',
-      'no-restricted-pre-release-devDependencies'
+      'no-restricted-pre-release-devDependencies',
+      'prefer-property-order'
     ];
 
     this.passedConfigParam = passedConfigParam;
diff --git a/src/rules/prefer-property-order.js b/src/rules/prefer-property-order.js
new file mode 100644
index 0000000..ad6dc73
--- /dev/null
+++ b/src/rules/prefer-property-order.js
@@ -0,0 +1,29 @@
+'use strict';
+
+const isInPreferredOrder = require('./../validators/property-order').isInPreferredOrder;
+const LintIssue = require('./../LintIssue');
+const lintId = 'prefer-property-order';
+const nodeName = '';
+const message = 'Your package.json properties are not in the desired order.';
+const ruleType = 'property-order';
+
+const lint = function(packageJsonData, lintType, preferredOrder) {
+  const result = isInPreferredOrder(packageJsonData, preferredOrder);
+
+  if (!result.status) {
+    let helpTip = '';
+
+    if (result.data.actualNode === null) {
+      helpTip = `Please add ${result.data.desiredNode} at the end of the file.`;
+    } else {
+      helpTip = `Please move ${result.data.actualNode} after ${result.data.desiredNode}.`;
+    }
+
+    return new LintIssue(lintId, lintType, nodeName, `${message} ${helpTip}`);
+  }
+
+  return true;
+};
+
+module.exports.lint = lint;
+module.exports.ruleType = ruleType;
diff --git a/src/validators/alphabetical-sort.js b/src/validators/alphabetical-sort.js
index 166b50f..592879a 100644
--- a/src/validators/alphabetical-sort.js
+++ b/src/validators/alphabetical-sort.js
@@ -27,7 +27,7 @@ const isInAlphabeticalOrder = function(packageJsonData, nodeName) {
   const nodeKeysOriginal = Object.keys(packageJsonData[nodeName]);
   const nodeKeysSorted = Object.keys(packageJsonData[nodeName]).sort();
 
-  for (let keyIndex = 0;keyIndex <= nodeKeysOriginal.length;keyIndex += increment) {
+  for (let keyIndex = 0;keyIndex < nodeKeysOriginal.length;keyIndex += increment) {
     if (nodeKeysOriginal[keyIndex] !== nodeKeysSorted[keyIndex]) {
       isValid = false;
       data = {
diff --git a/src/validators/property-order.js b/src/validators/property-order.js
new file mode 100644
index 0000000..27f21e9
--- /dev/null
+++ b/src/validators/property-order.js
@@ -0,0 +1,43 @@
+'use strict';
+
+const increment = 1;
+
+/**
+ * Determines whether an array is in the specified order
+ * @param  {Object} packageJsonData     Valid JSON
+ * @param  {Array}  preferredNodeOrder  Preferred order of nodes
+ * @return {Object}                     Object containing the status and the node that is out of order, if applicable
+ */
+const isInPreferredOrder = function(packageJsonData, preferredNodeOrder) {
+  let isValid = true;
+  let data = {
+    actualNode: null,
+    desiredNode: null
+  };
+  const actualNodeList = Object.keys(packageJsonData);
+
+  for (let keyIndex = 0;keyIndex < preferredNodeOrder.length;keyIndex += increment) {
+    if (typeof actualNodeList[keyIndex] === 'undefined') {
+      isValid = false;
+      data = {
+        actualNode: null,
+        desiredNode: preferredNodeOrder[keyIndex]
+      };
+      break;
+    } else if (actualNodeList[keyIndex] !== preferredNodeOrder[keyIndex]) {
+      isValid = false;
+      data = {
+        actualNode: actualNodeList[keyIndex],
+        desiredNode: preferredNodeOrder[keyIndex]
+      };
+      break;
+    }
+  }
+
+  return {
+    status: isValid,
+    data
+  };
+};
+
+module.exports.isInPreferredOrder = isInPreferredOrder;


Test patch:
diff --git a/tests/unit/Config.test.js b/tests/unit/Config.test.js
index c1d6257..22991ac 100755
--- a/tests/unit/Config.test.js
+++ b/tests/unit/Config.test.js
@@ -387,7 +387,8 @@ describe('Config Unit Tests', function() {
           'require-version': 'warning',
           'valid-values-author': ['error', ['Thomas', 'Lindner', 'Thomas Lindner']],
           'valid-values-private': ['warning', [true, false]],
-          'valid-values-license': ['error', ['private', 'unlicensed']]
+          'valid-values-license': ['error', ['private', 'unlicensed']],
+          'prefer-property-order': ['error', ['name', 'version']]
         };
         const config = new Config(rcFileObj);
 
diff --git a/tests/unit/rules/prefer-property-order.test.js b/tests/unit/rules/prefer-property-order.test.js
new file mode 100644
index 0000000..0c7204f
--- /dev/null
+++ b/tests/unit/rules/prefer-property-order.test.js
@@ -0,0 +1,67 @@
+'use strict';
+
+const chai = require('chai');
+const lint = require('./../../../src/rules/prefer-property-order').lint;
+
+const should = chai.should();
+
+describe('prefer-property-order Unit Tests', function() {
+  context('when the properties in the package.json file are in the desired order', function() {
+    it('true should be returned', function() {
+      const packageJsonData = {
+        name: 'awesome-module',
+        version: '1.0.0',
+        description: 'description'
+      };
+      const preferredOrder = [
+        'name',
+        'version',
+        'description'
+      ];
+      const response = lint(packageJsonData, 'error', preferredOrder);
+
+      response.should.be.true;
+    });
+  });
+
+  context('when the actual node list does not have the same number of nodes as the desired list', function() {
+    it('LintIssue object should be returned', function() {
+      const packageJsonData = {
+        name: 'awesome-module',
+        version: '1.0.0'
+      };
+      const preferredOrder = [
+        'name',
+        'version',
+        'description'
+      ];
+      const response = lint(packageJsonData, 'error', preferredOrder);
+
+      response.lintId.should.equal('prefer-property-order');
+      response.lintType.should.equal('error');
+      response.node.should.equal('');
+      response.lintMessage.should.equal('Your package.json properties are not in the desired order. Please add description at the end of the file.');
+    });
+  });
+
+  context('when the actual node list is in a different order than desired', function() {
+    it('LintIssue object should be returned', function() {
+      const packageJsonData = {
+        name: 'awesome-module',
+        description: 'description',
+        version: '1.0.0'
+      };
+      const preferredOrder = [
+        'name',
+        'version',
+        'description'
+      ];
+      const response = lint(packageJsonData, 'error', preferredOrder);
+
+      response.lintId.should.equal('prefer-property-order');
+      response.lintType.should.equal('error');
+      response.node.should.equal('');
+      response.lintMessage.should.equal('Your package.json properties are not in the desired order. Please move description after version.');
+    });
+  });
+});
diff --git a/tests/unit/validators/property-order.test.js b/tests/unit/validators/property-order.test.js
new file mode 100644
index 0000000..db3d147
--- /dev/null
+++ b/tests/unit/validators/property-order.test.js
@@ -0,0 +1,69 @@
+'use strict';
+
+const chai = require('chai');
+const propertyOrder = require('./../../../src/validators/property-order');
+
+const should = chai.should();
+
+describe('property-order Unit Tests', function() {
+  describe('isInPreferredOrder method', function() {
+    context('when the properties in the package.json file are in the desired order', function() {
+      it('true should be returned', function() {
+        const packageJson = {
+          name: 'awesome-module',
+          version: '1.0.0',
+          description: 'description'
+        };
+        const preferredOrder = [
+          'name',
+          'version',
+          'description'
+        ];
+        const response = propertyOrder.isInPreferredOrder(packageJson, preferredOrder);
+
+        response.status.should.be.true;
+        (response.data.actualNode === null).should.be.true;
+        (response.data.desiredNode === null).should.be.true;
+      });
+    });
+
+    context('when the actual node list does not have the same number of nodes as the desired list', function() {
+      it('false should be returned', function() {
+        const packageJson = {
+          name: 'awesome-module',
+          version: '1.0.0'
+        };
+        const preferredOrder = [
+          'name',
+          'version',
+          'description'
+        ];
+        const response = propertyOrder.isInPreferredOrder(packageJson, preferredOrder);
+
+        response.status.should.be.false;
+        (response.data.actualNode === null).should.be.true;
+        response.data.desiredNode.should.equal('description');
+      });
+    });
+
+    context('when the actual node list is in a different order than desired', function() {
+      it('false should be returned', function() {
+        const packageJson = {
+          name: 'awesome-module',
+          description: 'description',
+          version: '1.0.0'
+        };
+        const preferredOrder = [
+          'name',
+          'version',
+          'description'
+        ];
+        const response = propertyOrder.isInPreferredOrder(packageJson, preferredOrder);
+
+        response.status.should.be.false;
+        response.data.actualNode.should.equal('description');
+        response.data.desiredNode.should.equal('version');
+      });
+    });
+  });
+});