Custom Gulp.js plugin: TypeError "Path must be a string. Received undefined"

637 views Asked by At

I've found several threads on StackOverflow about TypeError: Path must be a string, even though I've not been able to apply the suggested solutions to my very case.

I'm trying to build a Gulp plugin that connects to the (paid) javascriptobfuscator.com service and use their POST API to obfuscate my JS.

The Gulp task looks like:

var gulp = require('gulp'),
    jso = require('./jsobfuscator');

gulp.task('jso', function() {
    // .jsx contain extendscript language, which as far as 
    // the service is concerned, could be treated as .js
    return gulp.src('.src/app/jsx/photoshop.jsx')
               .pipe(jso())
               .pipe(gulp.dest('./dist'))
});

The jsobfuscator.js file contains the following code:

var through = require('through2'),
    http = require('http'),
    gutil = require('gulp-util'),
    Readable = require('stream').Readable

module.exports = function() {
  /**
   * @this {Transform}
   */

    var transform = function(file, encoding, callback) {

        var that = this;

        var proj = {};
        proj.APIKey = "/* MyAPIKey*/";
        proj.APIPwd = "/* MyAPIPwd*/";
        proj.Name = "DoubleUSM";
        proj.ReorderCode = true;
        proj.ReplaceNames = true;
        proj.EncodeStrings = true;
        proj.MoveStrings = true;
        proj.MoveMembers = true;
        proj.DeepObfuscate = true;
        proj.SelfCompression = true;
        // Unsure about these two...
        proj.CompressionRatio = "Auto";
        proj.OptimizationMode = "Auto";

        var appJS = new Object();
        appJS.FileName = "app.js";
        appJS.FileCode = file.contents.toString('utf8');
        // Will try to implement multiple files later on
        proj.Items = [appJS];

        var postData = JSON.stringify(proj);

        // Length is OK
        gutil.log ("Length: " + Buffer.byteLength(postData, 'utf-8'))

        var options = {
            host: 'service.javascriptobfuscator.com',
            path: '/HttpApi.ashx',
            method: 'POST',
            headers: { 
                'Content-Type': 'text/json',
                'Content-Length': Buffer.byteLength(postData, 'utf-8')
            }
        };

        callback = function(response) {
            response.setEncoding('utf8');

            var str = '';
            response.on('data', function(chunk) {
                str += chunk;
            });

            response.on('end', function() {
                // I get the correct response here!
                gutil.log(JSON.stringify(JSON.parse(str), null, '  '));

                var resObj = JSON.parse(str);
                // Converting the received string into a Buffer
                var fileStream = new Readable();
                // resObj.Items[0].FileCode is where the obfuscated code belongs
                fileStream.push(resObj.Items[0].FileCode);
                fileStream.push(null);
                that.push(fileStream);
                callback();
            });
        }

        var req = http.request(options, callback);
        req.write(postData);
        req.end();
    };

    return through.obj(transform);
};

When I run the gulp task I apparently get the proper response from the javascriptobfuscator code (as you can see below) yet there's something wrong in the part where I pass along the file to the destination because I'm getting:

(master)*  gulp jso
[15:13:30] Using gulpfile ~/Dropbox/Developer/PROJECTS/DOUBLE USM/gulpfile.js
[15:13:30] Starting 'jso'...
[15:13:31] Lenght: 21897
[15:13:32] {
  "Type": "Succeed",
  "Items": [
    {
      "FileName": "app.js",
      "FileCode": /* ... Long, horrible, properly obfuscated blob here... */
    }
  ],
  "ErrorCode": null,
  "Message": null,
  "FileName": null,
  "LineNumber": null,
  "ExceptionToString": null
}
path.js:7
    throw new TypeError('Path must be a string. Received ' + inspect(path));
    ^

TypeError: Path must be a string. Received undefined
    at assertPath (path.js:7:11)
    at Object.resolve (path.js:1146:7)
    at DestroyableTransform.saveFile [as _transform] (/Users/davidebarranca/Dropbox/Developer/PROJECTS/DOUBLE USM/node_modules/vinyl-fs/lib/dest/index.js:36:26)
    at DestroyableTransform.Transform._read (/Users/davidebarranca/Dropbox/Developer/PROJECTS/DOUBLE USM/node_modules/vinyl-fs/node_modules/readable-stream/lib/_stream_transform.js:184:10)
    at DestroyableTransform.Transform._write (/Users/davidebarranca/Dropbox/Developer/PROJECTS/DOUBLE USM/node_modules/vinyl-fs/node_modules/readable-stream/lib/_stream_transform.js:172:12)
    at doWrite (/Users/davidebarranca/Dropbox/Developer/PROJECTS/DOUBLE USM/node_modules/vinyl-fs/node_modules/readable-stream/lib/_stream_writable.js:237:10)
    at writeOrBuffer (/Users/davidebarranca/Dropbox/Developer/PROJECTS/DOUBLE USM/node_modules/vinyl-fs/node_modules/readable-stream/lib/_stream_writable.js:227:5)
    at DestroyableTransform.Writable.write (/Users/davidebarranca/Dropbox/Developer/PROJECTS/DOUBLE USM/node_modules/vinyl-fs/node_modules/readable-stream/lib/_stream_writable.js:194:11)
    at DestroyableTransform.ondata (/Users/davidebarranca/Dropbox/Developer/PROJECTS/DOUBLE USM/node_modules/through2/node_modules/readable-stream/lib/_stream_readable.js:546:20)
    at emitOne (events.js:96:13)

Admittedly I'm not an expert of Gulp plugins – I'm trying to build a functionality for my own private use. It looks like I'm quite close, but I've been stuck in this dead end for a while and I need help from more experienced ones.

Thanks in advance


Half-working update

Modifying the response.on('end') callback as follows, I've been able to finally write the file:

response.on('end', function() {

    var resObj = JSON.parse(str);

    var fileStream = new Readable();
    fileStream.push(resObj.Items[0].FileCode);
    fileStream.push(null);

    // Manually creating a new Vinyl file
    var jsFile = new Vinyl({
        cwd: file.cwd,
        base: file.base,
        path: file.path,
        contents: fileStream
    });

    return that.push(jsFile);
    // callback();
});

I had to comment out the final callback(), though, otherwise I would have run into an error pointing to the response.setEncoding('utf8'); line, being response undefined.

The problem is now that it seems like the task, in some way, never terminates. The console says Starting 'jso'..., the file is written, the console returns, but there's no Finished 'jso' after nn ms, and worst of all, I'm completely unable to chain more gulp tasks afterwards, so if I:

gulp.task('jso1', function() {
    return gulp.src('.src/app/jsx/photoshop.jsx')
               .pipe(jso())
               .pipe(gulp.dest('./dist'))
});

gulp.task('jso2', ['jso1'], function() {
    return gulp.src('.src/app/app.js')
               .pipe(jso())
               .pipe(gulp.dest('./dist'))
});

// then in the Terminal:
$ gulp jso2

The only thing that I get is jso1 starting, apparently working, but never finishing; jso2 never runs.


1

There are 1 answers

0
Davide Barranca On

Many hours of head scratching made me get to the answer. Briefly, in my Gulp plugin:

  1. I had to return a new Vinyl file in the the response.on('end') callback: the file's contents property needs to be the stream from the server response.
  2. My half-working solution's main flaw is that I've given the same name (callback) to two very different functions. One is the callback from the initial transform function; the other one is the http.request callback. Commenting out the last callback() call, after return that.push(jsFile); prevented the whole Gulp task from ending.

The full working code is as follows:

var through  = require('through2'),
    http     = require('http'),
    gutil    = require('gulp-util'),
    Readable = require('stream').Readable,
    Vinyl    = require('vinyl');

module.exports = function() {

    var transform = function(file, encoding, callback) {

        if (file.isNull()) { return callback(null, file) }

        var that = this;

        var proj = {};
        proj.APIKey = "/* MyAPIKey */";
        proj.APIPwd = "/* APIPwd */";
        proj.Name = "Project";
        proj.ReorderCode = true;
        proj.ReplaceNames = true;
        proj.EncodeStrings = true;
        proj.MoveStrings = true;
        proj.MoveMembers = true;
        proj.DeepObfuscate = true;
        proj.SelfCompression = true;
        proj.CompressionRatio = "Auto";
        proj.OptimizationMode = "Auto";

        var appJS = new Object();
        appJS.FileName = "app.js";
        appJS.FileCode = file.contents.toString('utf8');

        proj.Items = [appJS];

        var postData = JSON.stringify(proj);

        var options = {
            host: 'service.javascriptobfuscator.com',
            path: '/HttpApi.ashx',
            method: 'POST',
            headers: { 
                'Content-Type': 'text/json',
                'Content-Length': Buffer.byteLength(postData, 'utf-8')
            }
        };

        /* Renamed to avoid confusion with transform's callback */
        postCallback = function(response) {

            response.setEncoding('utf8');
            var str = '';
            response.on('data', function(chunk) {
                str += chunk;
            });

            response.on('end', function() {

                var resObj = JSON.parse(str);
                var fileStream = new Readable();
                fileStream.push(resObj.Items[0].FileCode);
                fileStream.push(null);

                /* Return a new Vinyl File */
                var jsFile = new Vinyl({
                    cwd: file.cwd,
                    base: file.base,
                    path: file.path,
                    contents: fileStream
                });

                that.push(jsFile);
                callback();
            });
        }

        var req = http.request(options, postCallback);
        req.write(postData);
        req.end();

    };

    return through.obj(transform);
};

Perhaps there might be better ways to return the Vinyl file without creating a new one.

Another improvement would be to group all the source files and make a single http call to javascriptobfuscator API (it is possible in fact to add multiple files to the proj.Items array), but I have no idea how to group files in the source, pass them all at once to the Gulp plugin, and then split them back later.