How do I emit an event into gulp-tap's parent stream - javascript

I'm setting up a gulpfile for bundling multiple JavaScript files into multiple bundles during development.
Since I want to watch this process and not have it exit if there is an error (syntax or something) in one of the files I would need to properly emit an end event to the parent stream.
For handling the multiple files I am using the approach described in the gulp recipes.
But using this and reading the docs on gulp-tap I am unsure how to have it emit the error into the parent stream.
What I am trying to do is the following:
gulp.task('bundle', () => (
gulp.src(['whatevs/*.js'], { read: false })
.pipe($.tap((file) => {
file.contents = browserify( // eslint-disable-line no-param-reassign
file.path,
{ debug: true, fullPaths: true }
).bundle();
}))
.on('error', function handleBrowserifyError(err) {
this.emit('end');
})
.pipe(gulp.dest('bundles/'))
));
I can see the error if I would pass a callback to the bundle() call, but I do not have the slightest clue how to get it back into the parent stream.

I managed to do it like this:
gulp.task('bundle', () => {
const scriptStream = gulp.src(['whatevs/*.js'], { read: false })
.pipe($.tap((file) => {
file.contents = browserify( // eslint-disable-line no-param-reassign
file.path,
{ debug: true, fullPaths: true }
)
.bundle()
.on('error', () => scriptStream.emit('end'));
}))
.on('error', Function.prototype) // prevents crashing
.pipe(gulp.dest('bundles/'));
return scriptStream
});

Related

Gulp 4 Default Task with Series and Parallel

I've looked at many different posts here on SO using Gulp
CLI version: 2.3.0
Local version: 4.0.2
and have tried multiple approaches to the gulp 4 default task declaration:
exports.default = gulp.series(clean, (done) => {
gulp.parallel(styles, scripts);
done();
});
gulp.task('default',
gulp.series(clean, (done) => {
gulp.parallel(scripts, styles);
done();
});
);
function production () {
return gulp.series(clean, function (done) {
gulp.parallel(styles, scripts);
done();
})
}
exports.default = production
Clean function is simple:
function clean(cb) {
console.log("clean");
del(["dist"]);
cb();
}
For testing I have created a test function:
function test() {
console.log("in test");
return (
gulp.src(paths.styles.src)
.pipe(notify("Styles rendered"))
);
}
exports.test = test;
And a test default function:
exports.default = gulp.series(clean, (done) => {
console.log("before styles and scripts");
gulp.parallel(test);
console.log("after styles and scripts");
done();
});
Printed to the terminal are:
"clean"
"before styles and scripts"
"after styles and scripts"
But no "in test".
What am I missing here?
Not sure if this is the way Gulp is designed, but based on this answer, the code that works for me is:
exports.default = gulp.series(clean, (callbackA) => {
return gulp.parallel(styles, scripts, (callbackB) =>
{
callbackA();
callbackB();
})();
});
And the three tasks i'm calling are:
function clean(cb) {
del(["dist"]);
cb();
}
// Define tasks after requiring dependencies
function styles() {
console.log("in styles");
return (
gulp.src(paths.styles.src)
.pipe(sass())
.on("error", sass.logError)
.pipe(sourcemaps.init())
.pipe(sass())
.on("error", sass.logError)
.pipe(postcss([autoprefixer(), cssnano()]))
.pipe(sourcemaps.write())
.pipe(gulp.dest(paths.styles.dest))
// Add browsersync stream pipe after compilation
.pipe(browserSync.stream())
);
}
// Expose the task by exporting it
// This allows you to run it from the commandline using
// $ gulp style
exports.styles = styles;
function scripts() {
// Start by calling browserify with our entry pointing to our main javascript file
return ( gulp.src(paths.scripts.src)
.pipe(babel())
.pipe(uglify())
// Then write the resulting files to a folder
.pipe(gulp.dest(paths.scripts.dest))
);
}
// Expose the task, this allows us to call this task
// by running `$ gulp build' in the terminal
exports.scripts = scripts;
Aside from Gulp docs and here, this was a nice source.

Node.js: Error handling when using pipe with streams

I am reading large CSV and transforming it to create multiple other csv's. Here is my relevant part of the code. How do i error handle each step in this case?
I have added a generic on('error') but that does not seem to get triggered always.
Does each chain in the below code need its own on('error')? or is there a more elegant way to do this.
await fs.createReadStream(m.path)
.pipe(csv.parse({delimiter: '\t', columns: true}))
.pipe(csv.transform((input) => {
//delete input['Date'];
console.log(input);
return input;
}))
.pipe(csv.stringify({header: true}))
.pipe(fs.createWriteStream(transformedPath))
.on('finish', () => {
console.log('finish....');
}).on('error', () => {
console.log('error.....');
});
Thanks.
The stream API require each "middleware" to have its own .on("error") handler.
However, NodeJS ships with a pipeline utility function for this use case (stream.pipeline). The pipeline handles errors and closes streams correctly.
Using a pipeline, the example code provided by the OP would be written as:
import stream from "stream";
import util from "util";
try {
await util
.promisify(stream.pipeline)(
fs.createReadStream(m.path),
csv.parse({delimiter: '\t', columns: true})
csv.transform((input) => {
//delete input['Date'];
console.log(input);
return input;
}),
csv.stringify({header: true}),
fs.createWriteStream(transformedPath)
);
console.log('finish....');
}
catch( error ){
console.log('error.....');
}

Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL in the protractor

I am using a protractor and ECMAScript 6 to develop end to end tests. But when the test is finished I see the error:
Message:
Error: Timeout - Async callback was not invoked within the timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
I tried all solutions that I find on the StackOverflow and GitHub. For example, an add jasmineNodsOption to the config file, add parameters done to the test function, add timeouts to the functions nothing help.
test.js
import { doubleClick, isDisplayed, waitToBeVisible } from '../page_objects/base_page';
import HairLossPage from '../page_objects/HairLossPage';
import CartComponent from '../page_objects/components/CartComponent';
/*global expect describe beforeAll it browser*/
describe('Hair-loss. Question pages', () => {
const heirLoss = new HairLossPage();
const cartComponent = new CartComponent();
beforeAll(() => {
browser.waitForAngularEnabled(false);
heirLoss.get();
});
it('Check the "CheckOut" button in the mini-cart with product', () => {
doubleClick(heirLoss.header.card);
waitToBeVisible(cartComponent.header.title);
expect(isDisplayed(cartComponent.header.title)).toBe(true);
expect(isDisplayed(cartComponent.content.shopHair)).toBe(true);
expect(isDisplayed(cartComponent.content.shopEd)).toBe(true);
expect(isDisplayed(cartComponent.content.shopSkin)).toBe(true);
expect(isDisplayed(cartComponent.content.shopDailyHealt)).toBe(true);
});
});
config.js
require("#babel/register");
require("protractor");
exports.config = {
seleniumAddress: 'http://localhost:4444/wd/hub',
specs: ['./src/tests/**.e2e.js'],
multiCapabilities: [{
browserName: 'chrome'
}],
//Global configuration
baseUrl: 'https:/facebook.com/',
//Allure configuration
/*global jasmine browser allure*/
onPrepare: () => {
browser.manage().window().maximize();
var AllureReporter = require('jasmine-allure-reporter');
jasmine.getEnv().addReporter(new AllureReporter());
jasmine.getEnv().afterEach((done) => {
if (done.status === "failed") {
browser.takeScreenshot().then((png) => {
allure.createAttachment('Screenshot', () => new Buffer(png, 'base64'), 'image/png')();
done();
});
}
});
}
};
I am thinking isDisplayed and waitToBeVisible are asynchronous and are getting stuck somewhere.
Try:
beforeAll(() => {
console.log('Before waiting for angular enabled');
browser.waitForAngularEnabled(false);
console.log('After waiting for angular enabled, before heirLoss.get');
heirLoss.get();
console.log('After heirLoss.get');
});
it('Check the "CheckOut" button in the mini-cart with product', () => {
console.log('before doubleClick');
doubleClick(heirLoss.header.card);
console.log('after doubleClick, before waitToBeVisible');
waitToBeVisible(cartComponent.header.title);
console.log('after waitToBeVisible, before first isDisplayed');
expect(isDisplayed(cartComponent.header.title)).toBe(true);
console.log('after first isDisplayed, before 2nd isDisplayed');
// keep adding console.log before and after these assertions and see where it gets stuck.
expect(isDisplayed(cartComponent.content.shopHair)).toBe(true);
expect(isDisplayed(cartComponent.content.shopEd)).toBe(true);
expect(isDisplayed(cartComponent.content.shopSkin)).toBe(true);
expect(isDisplayed(cartComponent.content.shopDailyHealt)).toBe(true);
});
Once you have a good idea of where it gets stuck, you can have a better idea of why it's getting stuck there.

Promises in test cases in mocha and chai failing

I am writing a test case in mocha and chai to check if the file is not present it will create the file. Following is the test case :
context('if the valid message is supplied and file is not present in the app\'s logs folder', () => {
beforeEach((done) => {
setTimeout(() => {
fs.exists(filePath, (exists) => {
if (exists) {
fileFound = true;
} else {
fileFound = false;
}
});
done();
}, 100);
});
it('should indicate the file is not present in the app\'s log folder', () => {
expect(fileFound).to.be.false;
});
it('should create a new file in the app\'s log folder', () => {
expect(fileFound).to.be.true;
});
});
Let's day file is present in the folder, in that case first test case should fail. But the problem is, it is saying expected undefined to be false, rather than expected true to be false.
There's very little point in using promises here. Your API is callback-based, so you should use a callback test.
Like this:
it('should exist', (done) => {
fs.exists(filePath, (exists) => {
expect(exists).to.be.true;
done();
});
});
One thing to bear in mind, mostly unrelated to your issue, is that fs.exists is deprecated and you should use a different method like fs.access or fs.stat:
it('should exist', (done) => {
fs.access(filePath, (err) => {
expect(err).to.be.null;
done();
});
});
To address your post edit question, the problem here is that you are using setTimeout for no reason and calling done before fs.exists has a chance to finish.
The solution: get rid of the setTimeout and call done inside the fs.exists callback. You should also scope your fileFound variable at a place where it makes sense:
context('if the valid message is supplied and file is not present in the app\'s logs folder', () => {
let fileFound;
beforeEach((done) => {
fs.exists(filePath, (exists) => {
fileFound = exists;
done();
});
});
it('should indicate the file is not present in the app\'s log folder', () => {
expect(fileFound).to.be.false;
});
it('should create a new file in the app\'s log folder', () => {
expect(fileFound).to.be.true;
});
});

Wait Until Webpack Dev Server Is Ready

Requirements
I need to run the webpack-dev-server and wait until the server is ready to serve pages.
Solution
// Start a webpack-dev-server
new WebpackDevServer(webpack(myConfig), {
publicPath: myConfig.output.publicPath,
hot: true,
historyApiFallback: true,
// It suppress error shown in console, so it has to be set to false.
quiet: false,
// It suppress everything except error, so it has to be set to false as well
// to see success build.
noInfo: false,
stats: {
// Config for minimal console.log mess.
assets: false,
colors: true,
version: false,
hash: false,
timings: false,
chunks: false,
chunkModules: false
}
}).listen(3000, '0.0.0.0', function(err) {
if(err) throw new gutil.PluginError('webpack-dev-server', err);
gutil.log('[webpack-dev-server]', 'http://0.0.0.0:3000/webpack-dev-server/index.html');
//this is to ensure that end to end test wouldn't start running until the server is ready
http.get({
hostname: '0.0.0.0',
port: 3000,
path: '/',
agent: false // create a new agent just for this one request
}, (/*res*/) => {
// Do stuff with response
done();
});
});
Problem
On Linux a have a wait until the server is ready. On Windows I get an exception because there is no wait and the server is not ready
C:\development\ucms-react>gulp webpack-dev-server [11:03:01] Requiring
external module babel-register [11:03:07] Using gulpfile
C:\development\ucms-react\gulpfile.babel.js [11:03:07] Starting
'webpack-dev-server'... [11:03:07] [webpack-dev-server]
http://0.0.0.0:3000/webpack-dev-server/index.html error events.js:141
throw er; // Unhandled 'error' event
^
Error: connect EADDRNOTAVAIL 0.0.0.0:3000
at Object.exports._errnoException (util.js:907:11)
at exports._exceptionWithHostPort (util.js:930:20)
at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1077:14)
C:\development\ucms-react>
How can I solve this issue?
The other answer here by Brett DeWoody is partially correct. It does properly wait until the server is started. However this does not wait for the bundle to be created. So if you ask webpack for a page at this point, it will return something akin to "still compiling".
To wait for the bundle to be fully compiled (and that the server is listening) before trying to access a page, you'll need something like this:
function makeServer(config) {
return new Promise((resolve, reject) => {
const compiler = webpack(config);
let compiled = false;
let listening = false;
compiler.hooks.done.tap('IDoNotUnderstandWhatThisStringIsForButItCannotBeEmpty', () => {
// console.log('compiled');
if (listening) resolve(server);
else compiled = true;
});
const server = new WebpackDevServer(compiler, config.devServer);
server.listen(port, '0.0.0.0', err => {
if (err) return reject(err);
// console.log('listening');
if (compiled) resolve(server);
else listening = true;
});
});
}
Then, just like Brett's answer, await the result:
var server = await makeServer(config);
Or without async/await:
makeServer(config).then(server => {
// Do something with server
});
Here's another solution (inspired by #CameronTacklind's answer). The setImmediate makes the log output show up below the initial compiler log output.
const PORT = process.env.PORT || 3000
const compiler = webpack(config)
new WebpackDevServer(compiler).listen(PORT, '0.0.0.0', err => {
if (err) {
console.log(err)
}
})
compiler.hooks.done.tap('done', () => {
setImmediate(() => {
console.log()
console.log(`Running at http://localhost:${PORT}`)
})
})
UPDATE
Here's an alternative version that doesn't require a custom server script—you can just add this to your webpack config options. It also preserves the built-in feature of automatically using a different port if the default one isn't available...of course, if you'd rather specify your port, you don't have to use that part.
const chalk = require('chalk')
...
module.exports = {
....
devServer: {
onListening: server => {
const { port } = server.listeningApp.address()
server.compiler.hooks.done.tap('done', () => {
setImmediate(() => {
console.log()
console.log(
chalk.cyan.bold(`Running at http://localhost:${port}`)
)
})
})
},
}
}
One way to handle this is to wrap the server setup in a function that returns a Promise. The Promise is resolved when the server connects, or is rejected if there's an error.
Here's a simplified example:
function startServer() {
return new Promise((resolve, reject) => {
new WebpackDevServer(webpack(myConfig), {
// Do stuff
}).listen(3000, '0.0.0.0', function(err) {
resolve();
}).on('error', (error) => {
reject(error);
});
});
}
and then provide a callback for when the server is started:
var server = startServer();
server.then(function() {
// Run tests
});

Categories