Repository: opencomponents/oc (javascript)

Issue:
Add cli argument to skip dependencies packaging
In some scenarios people tend to compile dependencies by their own before doing `oc publish`, so let's add a cli argument, something like `--skip-dependencies` or `--skip-build`, to allow us skip the dependencies installation later again via oc.


Discussion:
If I start working on it I'd assign it to me. Until then, I leave it open for others to collaborate.
So, given we have an `oc package` command (which contains the `_package` folder), perhaps I would name this `--skip-package` - so it would just publish whatever is inside `_package` (skipping the dependencies' check and all of that). What do you think?
I don't know the internals like you, so I guess your argument will probably fit better than mine :_D
@elboletaire look at @NimaSoroush's PR that we just merged. Sounds like very similar but for packaging (skips install and does symlinking) https://github.com/opencomponents/oc/pull/971
Thanks @matteofigus. Thing is I wasn't able to dedicate more time to this since last time, but I'll look into it as soon as I've time again :smiley: 
I was thinking on taking on this issue and I think there's 3 use cases to consider when doing this new command.

## 1. Mirroring what publish does on the same component
`oc package your-component-folder`
`oc publish your-component-folder --skip-package`

In this case the scenario is you have your component, you package first, it creates the `_package` folder, so when you run the second command, it just tries to find a `_package` folder inside, and then it will compress, and publish it.

## 2. Publishing the folder itself
`oc package your-component-folder`
`... store the _package folder somewhere to be published later`
`oc publish _package --skip-package`

In this case, the component is already gone, as we only stored the artifact of the package to be deployed/published at a later stage. So here the CLI will see that there's no _package folder inside of your path, so will try to find a package.json inside, and if there is one, check that we are on a compiled package (checking that theres an `oc.packaged = true` could do the trick here) and publish those files.

## 3. Publishing a compressed file
`oc package your-component-folder --compress`
`... store the tar.gz file somewhere to be published later`
`oc publish your-file.tar.gz`

Similar to the case 2 but here we also compress and we want to publish that file. In this case I don't see the point of skipping package, since CLI could already assume that if the path is of a compressed file, you just want to publish that.

Does this approach make sense?

Patch:
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npx --no-install lint-staged
diff --git a/.prettierrc b/.prettierrc
--- a/.prettierrc
+++ b/.prettierrc
@@ -4,5 +4,5 @@
   "semi": true,
   "singleQuote": true,
   "useTabs": false,
-  "arrowParens": "always"
+  "arrowParens": "avoid"
 }
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -13,17 +13,16 @@
     "publish-major": "npm run version-major && npm run test-silent && npm run git-stage-and-push && npm run npm-publish",
     "publish-minor": "npm run version-minor && npm run test-silent && npm run git-stage-and-push && npm run npm-publish",
     "publish-patch": "npm run version-patch && npm run test-silent && npm run git-stage-and-push && npm run npm-publish",
-    "precommit": "lint-staged",
     "test": "npm run build && node tasks/mochaTest.js",
     "test-silent": "npm run build && node tasks/mochaTest.js --silent",
     "version-major": "node tasks/version.js --type=\"major\"",
     "version-minor": "node tasks/version.js --type=\"minor\"",
-    "version-patch": "node tasks/version.js --type=\"patch\""
+    "version-patch": "node tasks/version.js --type=\"patch\"",
+    "prepare": "husky install"
   },
   "lint-staged": {
     "*.js": [
-      "prettier-eslint --single-quote --write",
-      "git add"
+      "prettier-eslint --single-quote --write"
     ]
   },
   "engines": {
@@ -48,9 +47,9 @@
     "chai": "4.2.0",
     "chalk": "^2.1.0",
     "glob": "^7.1.2",
-    "husky": "3.0.9",
+    "husky": "^7.0.0",
     "injectr": "0.5.1",
-    "lint-staged": "9.4.2",
+    "lint-staged": "^11.1.2",
     "minimist": "^1.2.0",
     "mocha": "7.0.1",
     "node-emoji": "^1.8.1",
diff --git a/src/cli/commands.js b/src/cli/commands.js
--- a/src/cli/commands.js
+++ b/src/cli/commands.js
@@ -127,6 +127,11 @@ module.exports = {
         username: {
           description:
             'username used to authenticate when publishing to registry'
+        },
+        skipPackage: {
+          boolean: true,
+          description: 'Skip packaging step',
+          default: false
         }
       },
       description: 'Publish a component',
diff --git a/src/cli/domain/local.js b/src/cli/domain/local.js
--- a/src/cli/domain/local.js
+++ b/src/cli/domain/local.js
@@ -1,7 +1,6 @@
 'use strict';
 
 const fs = require('fs-extra');
-const path = require('path');
 const targz = require('targz');
 const _ = require('lodash');
 
diff --git a/src/cli/facade/publish.js b/src/cli/facade/publish.js
--- a/src/cli/facade/publish.js
+++ b/src/cli/facade/publish.js
@@ -4,6 +4,7 @@ const async = require('async');
 const colors = require('colors/safe');
 const format = require('stringformat');
 const path = require('path');
+const fs = require('fs-extra');
 const read = require('read');
 const _ = require('lodash');
 
@@ -18,6 +19,7 @@ module.exports = function(dependencies) {
 
   return function(opts, callback) {
     const componentPath = opts.componentPath,
+      skipPackage = opts.skipPackage,
       packageDir = path.resolve(componentPath, '_package'),
       compressedPackagePath = path.resolve(componentPath, 'package.tar.gz');
 
@@ -25,6 +27,9 @@ module.exports = function(dependencies) {
 
     callback = wrapCliCallback(callback);
 
+    const readPackageJson = cb =>
+      fs.readJson(path.join(packageDir, 'package.json'), cb);
+
     const getCredentials = function(cb) {
       if (opts.username && opts.password) {
         logger.ok(strings.messages.cli.USING_CREDS);
@@ -42,12 +47,17 @@ module.exports = function(dependencies) {
       });
     };
 
+    const compress = cb => {
+      local.compress(packageDir, compressedPackagePath, cb);
+    };
+
     const packageAndCompress = function(cb) {
       logger.warn(format(strings.messages.cli.PACKAGING, packageDir));
       const packageOptions = {
         production: true,
         componentPath: path.resolve(componentPath)
       };
+
       local.package(packageOptions, (err, component) => {
         if (err) {
           return cb(err);
@@ -57,7 +67,7 @@ module.exports = function(dependencies) {
           format(strings.messages.cli.COMPRESSING, compressedPackagePath)
         );
 
-        local.compress(packageDir, compressedPackagePath, err => {
+        compress(err => {
           if (err) {
             return cb(err);
           }
@@ -129,61 +139,78 @@ module.exports = function(dependencies) {
       });
     };
 
+    const publishToRegistries = (registryLocations, component) => {
+      async.eachSeries(
+        registryLocations,
+        (registryUrl, next) => {
+          const registryNormalised = registryUrl.replace(/\/$/, ''),
+            componentRoute = `${registryNormalised}/${component.name}/${component.version}`;
+          putComponentToRegistry(
+            { route: componentRoute, path: compressedPackagePath },
+            next
+          );
+        },
+        err => {
+          local.cleanup(compressedPackagePath, (err2, res) => {
+            if (err) {
+              return callback(err);
+            }
+            callback(err2, res);
+          });
+        }
+      );
+    };
+
     registry.get((err, registryLocations) => {
       if (err) {
         logger.err(err);
         return callback(err);
       }
 
-      handleDependencies(
-        { components: [path.resolve(componentPath)], logger },
-        (err, dependencies) => {
-          if (err) {
-            logger.err(err);
-            return callback(err);
-          }
-          packageAndCompress((err, component) => {
+      if (!skipPackage) {
+        handleDependencies(
+          { components: [path.resolve(componentPath)], logger },
+          err => {
             if (err) {
-              errorMessage = format(
-                strings.errors.cli.PACKAGE_CREATION_FAIL,
-                err
-              );
-              logger.err(errorMessage);
-              return callback(errorMessage);
+              logger.err(err);
+              return callback(err);
             }
-
-            async.eachSeries(
-              registryLocations,
-              (registryUrl, next) => {
-                const registryLength = registryUrl.length,
-                  registryNormalised =
-                    registryUrl.slice(registryLength - 1) === '/'
-                      ? registryUrl.slice(0, registryLength - 1)
-                      : registryUrl,
-                  componentRoute = format(
-                    '{0}/{1}/{2}',
-                    registryNormalised,
-                    component.name,
-                    component.version
-                  );
-
-                putComponentToRegistry(
-                  { route: componentRoute, path: compressedPackagePath },
-                  next
+            packageAndCompress((err, component) => {
+              if (err) {
+                errorMessage = format(
+                  strings.errors.cli.PACKAGE_CREATION_FAIL,
+                  err
                 );
-              },
-              err => {
-                local.cleanup(compressedPackagePath, (err2, res) => {
-                  if (err) {
-                    return callback(err);
-                  }
-                  callback(err2, res);
-                });
+                logger.err(errorMessage);
+                return callback(errorMessage);
               }
-            );
+
+              publishToRegistries(registryLocations, component);
+            });
+          }
+        );
+      } else {
+        if (fs.existsSync(packageDir)) {
+          readPackageJson((err, component) => {
+            if (err) {
+              logger.err(err);
+              return callback(err);
+            }
+            compress(err => {
+              if (err) {
+                logger.err(err);
+                return callback(err);
+              }
+
+              publishToRegistries(registryLocations, component);
+            });
           });
+        } else {
+          errorMessage = strings.errors.cli.PACKAGE_FOLDER_MISSING;
+          logger.err(errorMessage);
+          return callback(errorMessage);
         }
-      );
+      }
     });
   };
 };
diff --git a/src/resources/index.js b/src/resources/index.js
--- a/src/resources/index.js
+++ b/src/resources/index.js
@@ -32,7 +32,7 @@ ${yellow(validMockObject)}`;
 
 const initSuccess = (componentName, componentPath) => {
   const success = `Success! Created ${componentName} at ${componentPath}`;
-  return `${green(success)} 
+  return `${green(success)}
 
 From here you can run several commands
 
@@ -173,6 +173,8 @@ module.exports = {
         'the version of used OC CLI is invalid. Try to upgrade OC CLI running {0}',
       PACKAGE_CREATION_FAIL: 'An error happened when creating the package: {0}',
       PACKAGING_FAIL: 'an error happened while packaging {0}: {1}',
+      PACKAGE_FOLDER_MISSING:
+        'Could not find a _package folder to publish. Try running "oc package" first, or do not skip packaging',
       PLUGIN_MISSING_FROM_REGISTRY:
         'Looks  like you are trying to use a plugin in the dev mode ({0}).\nYou need to mock it doing {1}',
       PORT_IS_BUSY:


Test patch:
diff --git a/test/unit/cli-facade-publish.js b/test/unit/cli-facade-publish.js
--- a/test/unit/cli-facade-publish.js
+++ b/test/unit/cli-facade-publish.js
@@ -13,26 +13,41 @@ describe('cli : facade : publish', () => {
     Local = require('../../src/cli/domain/local'),
     local = new Local(),
     readStub = sinon.stub().yields(null, 'test'),
-    PublishFacade = injectr('../../src/cli/facade/publish.js', {
-      read: readStub
-    }),
-    publishFacade = new PublishFacade({
-      registry,
-      local,
-      logger: logSpy
-    });
-
-  const execute = function(cb, creds) {
-    creds = creds || {};
+    mockComponent = {
+      name: 'hello-world',
+      version: '1.0.0'
+    };
+
+  const execute = function(
+    cb,
+    { creds = {}, skipPackage = false, fs = {} } = {}
+  ) {
     logSpy.err = sinon.stub();
     logSpy.log = sinon.stub();
     logSpy.ok = sinon.stub();
     logSpy.warn = sinon.stub();
+    const fsMock = Object.assign(
+        {
+          existsSync: sinon.stub().returns(true),
+          readJson: sinon.stub().yields(null, mockComponent)
+        },
+        fs
+      ),
+      PublishFacade = injectr('../../src/cli/facade/publish.js', {
+        'fs-extra': fsMock,
+        read: readStub
+      }),
+      publishFacade = new PublishFacade({
+        registry,
+        local,
+        logger: logSpy
+      });
     publishFacade(
       {
         componentPath: 'test/fixtures/components/hello-world/',
         username: creds.username,
-        password: creds.password
+        password: creds.password,
+        skipPackage
       },
       () => {
         cb();
@@ -106,10 +121,7 @@ describe('cli : facade : publish', () => {
 
         describe('when a component is valid', () => {
           beforeEach(() => {
-            sinon.stub(local, 'package').yields(null, {
-              name: 'hello-world',
-              version: '1.0.0'
-            });
+            sinon.stub(local, 'package').yields(null, mockComponent);
           });
 
           afterEach(() => {
@@ -279,8 +291,10 @@ describe('cli : facade : publish', () => {
                 beforeEach(done => {
                   sinon.stub(registry, 'putComponent').yields('Unauthorized');
                   execute(done, {
-                    username: 'myuser',
-                    password: 'password'
+                    creds: {
+                      username: 'myuser',
+                      password: 'password'
+                    }
                   });
                 });
 
@@ -323,6 +337,60 @@ describe('cli : facade : publish', () => {
           });
         });
       });
+      describe('when skipping packaging', () => {
+        beforeEach(() => {
+          sinon.stub(local, 'compress').yields(null);
+        });
+
+        afterEach(() => {
+          local.compress.restore();
+        });
+
+        it('should publish the package to all registries', done => {
+          sinon.stub(registry, 'putComponent').yields(null, 'ok');
+          execute(
+            () => {
+              registry.putComponent.restore();
+
+              expect(logSpy.ok.args[0][0]).to.include('http://www.api.com');
+              expect(logSpy.ok.args[1][0]).to.include('http://www.api2.com');
+              done();
+            },
+            { skipPackage: true }
+          );
+        });
+        it('should skip packaging', done => {
+          sinon.stub(registry, 'putComponent').yields(null, 'ok');
+          sinon.stub(local, 'package');
+          execute(
+            () => {
+              registry.putComponent.restore();
+
+              expect(local.package.called).to.be.false;
+
+              local.package.restore();
+              done();
+            },
+            { skipPackage: true }
+          );
+        });
+        it('should show an error message if the package folder does not exist', done => {
+          execute(
+            () => {
+              expect(logSpy.err.args[0][0]).to.equal(
+                'Could not find a _package folder to publish. Try running "oc package" first, or do not skip packaging'
+              );
+              done();
+            },
+            {
+              skipPackage: true,
+              fs: {
+                existsSync: sinon.stub().returns(false)
+              }
+            }
+          );
+        });
+      });
     });
   });
 });