Write to same file from multiple tasks (Gulp, Node, gulp-json) - node.js

I've just started using Gulp (and NodeJs)... Obviously I ran into my first wall.
Here it is:
I have a large project that uses themes. Each theme has it's own assets (scss and js files). Here is my gulpfile.js:
// < require block here (not included, to keep this short)
var themes = ["theme1", "theme2", "theme3"];
// Since I can have up to 20 different themes, I use the 'themes' array so I can create tasks dynamically, like this:
themes.forEach(function (theme) {
gulp.task('css:' + theme, function () {
setVersion([theme], 'css'); // write asset version into a json file
gulp.src('../themes/frontend/' + theme + '/assets/css/style.scss')
.pipe(sourcemaps.init())
.pipe(sass({outputStyle: 'compressed'}).on('error', sass.logError))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest('../themes/frontend/' + theme + '/assets/css'))
});
});
// Of course, I need an "all" task to build all CSS in rare ocasions I need to do so:
gulp.task('css:all', ("css:" + themes.join(",css:")).split(","));
// ("css:" + themes.join(",css:")).split(",") => results in the needed ['css:theme1', 'css:theme2'] tasks array
// The same logic as above for JS files
// but added the use of gulp-concat and gulp-uglify
// Having scripts = { "theme1" : ['script1', 'script2'], "theme2": ['script1', 'script2'] }
// ...
// And "per theme" both "css and js"
themes.forEach(function (theme) {
gulp.task('theme:' + theme, ['css:' + theme, 'js:' + theme]);
});
// Next I need to set versions for each asset
// I'm writing all the versions into a json file
assetsVersion = someRandomGeneratedNumber;
function setVersion(themes, assetType) {
/**
* themes: array
* assetType: 'all', 'css' or 'js'
*/
var fs = require('fs'),
path = require("path");
var versionsFilePath = path.normalize(__dirname + '/../protected/config/theme/frontend/');
var versionsFileName = '_assets-version.json';
if (!fs.existsSync(versionsFilePath + versionsFileName)) {
// Create file if it doesn't exist
fs.writeFile(versionsFilePath + versionsFileName, "{}", function (err) {
if (err) {
return console.log(err);
}
});
}
gulp.src(versionsFilePath + versionsFileName)
.pipe(jeditor(function (json) {
themes.forEach(function(theme) {
if ("undefined" == typeof (json[theme])) {
json[theme] = {};
}
if ('css' == assetType) {
json[theme]['css'] = assetsVersion;
} else if ('js' == assetType) {
json[theme]['js'] = assetsVersion;
} else {
json[theme] = {"css": assetsVersion, "js": assetsVersion};
}
if ("undefined" == typeof(json[theme]['css'])) {
// if we're missing the 'css' key (i.e. we've just created the json file), add that too
json[theme]['css'] = assetsVersion;
}
if ("undefined" == typeof(json[theme]['js'])) {
// if we're missing the 'js' key (i.e. we've just created the json file), add that too
json[theme]['js'] = assetsVersion;
}
});
return json;
}))
.pipe(gulp.dest(versionsFilePath));
}
The assets versioning json should look like this:
{
"theme1": {
"css": "20150928163236",
"js": "20150928163236"
},
"theme2": {
"css": "20150928163236",
"js": "20150928163236"
},
"theme3": {
"css": "20150928163236",
"js": "20150928163236"
}
}
running 'gulp css:theme#' - works fine...
BUT running 'gulp css:all' - makes a messy json
Of course, this happens because all css:theme# (or js:theme#) tasks run async, and more often than not there are multiple tasks writing simultaneously to my json file.
I've read about tasks depending on other tasks, but that doesn't really fit into my whole "dynamic tasks" flow (or I don't know how to fit it in).
I mean I don't think that this:
gulp.task('css:theme1', ['versioning'], function() {
//do stuff after 'versioning' task is done.
});
would help me. SO what if it waits for the version to be written? Multiple tasks would still write to the file at the same time. Also, for this to work, I would need to pass parameters that I also don't know how to do... like:
gulp.task('css:'+theme, ['versioning --theme ' + theme], function() {
//do stuff after 'versioning' task is done.
});
Like I could make it work in the console. I know this isn't working, BUT would be really useful in some cases if it would somehow be possible to send parameters to the task in the task name.
Neither runSequence() { ... done(); }, I really don't see how could I make it work within my flow...
Please, anybody... help a newb...
How can I solve this, while:
Having tasks created dynamically;
Having one versioning json file for all themes.

Related

Using a gulp task output inside another task

I have two gulp tasks as following:
gulp.task("merge-json", () => {
return gulp
.src(
[
src_folder + "/modules/**/*.json",
src_folder + "/organisms/**/*.json",
src_folder + "/pages/**/*.json",
],
{
since: gulp.lastRun("merge-json"),
}
)
.pipe(plumber())
.pipe(merge({fileName: "data.json"}))
.pipe(gulp.dest(src_folder + "/datas/dist/"))
.pipe(browserSync.stream());
});
gulp.task("nunjucks", () => {
return gulp
.src([src_folder + "pages/**/*.njk"], {
base: src_folder + "pages",
since: gulp.lastRun("nunjucks"),
})
.pipe(plumber())
.pipe(data(() => JSON.parse(fs.readFileSync(src_folder + "datas/dist/data.json"))))
.pipe(nunjucks({path: src_folder}))
.pipe(gulp.dest(dist_folder))
.pipe(browserSync.stream());
});
The first task uses merge-json plugin to merge all the json files into a single one called data.json, and then this data.json will be used by the gulp-nunjucks-render plugin to generate html pages.
The problem I have here is that a file called data.json is generated in my project src, which will only be used for the nunjucks plugin.
My question is, Isn't there any way to use the output of the merge-json (data.json) plugin directly inside the nunjucks plugin?
This is not necessarily an ideal solution, but I will leave it here nonetheless.
Using map-stream, introduce a new stage in the pipeline that intercepts the contents of the merged JSON file buffer and store it into a shared variable.
const map = require('map-stream');
// a variable to store the merged JSON string
let mergedJsonString;
gulp.task("merge-json", () => {
return gulp
.src(
[
src_folder + "/modules/**/*.json",
src_folder + "/organisms/**/*.json",
src_folder + "/pages/**/*.json",
],
{
since: gulp.lastRun("merge-json"),
}
)
.pipe(plumber())
.pipe(merge({fileName: "data.json"}))
// only one file should ever be passed - data.json
.pipe(map((file, callback) => {
// capture the file buffer as a string
mergedJsonString = file.contents.toString()
// pass on the file to the next pipe, unmodified
callback(null, file)
}))
// can optionally be removed if not necessary to save to a file anymore
.pipe(gulp.dest(src_folder + "/datas/dist/"))
.pipe(browserSync.stream());
});
Now the nunjucks task will need to be modified to use a different data source and specify a gulp task dependency. The data source could instead be conditionally generated to use an old cached version if the merge-json task is not run prior; e.g. JSON.parse(typeof mergedJsonString !== "undefined" ? mergedJsonString : fs.readFileSync(src_folder + "datas/dist/data.json"))
// optionally use gulp.series to create a task dependency on "merge-json", ensuring it runs prior to the "nunjucks" task
gulp.task("nunjucks", gulp.series(["merge-json"], () => {
return gulp
.src([src_folder + "pages/**/*.njk"], {
base: src_folder + "pages",
since: gulp.lastRun("nunjucks"),
})
.pipe(plumber())
// source the JSON from a variable in memory; or if undefined, maybe change the implementation to use an old cached "/data/dists/data.json" file
.pipe(data(() => JSON.parse(mergedJsonString)))
.pipe(nunjucks({path: src_folder}))
.pipe(gulp.dest(dist_folder))
.pipe(browserSync.stream());
});
Might need a few minor tweaks for your specific use case, but this does meet the use case, even if it is hacky.

Copy specified files with condition using gulp

I am trying copy my vendor files to my dev folder using gulp. When I was in development mode, I want copy only the unminified files, if unminified is not present copy minified files. And in production mode I want copy minifed files if files are not present minify the normal files.
my folder structure
js
app.js
jquery
jquery.min.js
jquery.js
fontawesome
fontawesome.min.js
fontawesome.min.css
fonts.ttf...
Here my basic I had written.
var scriptsPath = '../vendor/';
function getFolders(dir) {
return fs.readdirSync(dir)
.filter(function(file) {
return fs.statSync(path.join(dir, file)).isDirectory();
});
}
gulp.task('vendor', function() {
var folders = getFolders(scriptsPath);
var cssFilter = $.filter('**/*.css')
var tasks = folders.map(function(folder) {
var jsFilter;
if (isProduction) {
jsFilter = $.filter('**/*.min.js');
} else {
jsFilter = $.filter(['**/*.js', '!**/*.min.js']);
}
return gulp.src(path.join(scriptsPath, '**/'))
.pipe(jsFilter)
.pipe($.if(useSourceMaps, $.sourcemaps.init()))
.pipe($.if(isProduction, $.uglify({preserveComments: 'some'})))
.on('error', handleError)
.pipe(jsFilter.restore())
.pipe(cssFilter)
.pipe($.if( isProduction, $.minifyCss() ))
.on('error', handleError)
.pipe(cssFilter.restore())
.on('error', handleError)
.pipe(gulp.dest(build.vendor.js));
});
return es.concat.apply(null, tasks);
});
I am trying the last two days using gulp-if& some methods. But not yet get the solution.Thanks in advance.
You are trying to cram way to much into your vendor task. The stuff you do with your JS files is completely unrelated to the stuff you do with your CSS files. That's hard to read.
Instead of using gulp-filter try splitting vendor up into smaller tasks like vendor-js, vendor-css, etc... and then declare them as dependencies for your vendor task:
gulp.task('vendor', ['vendor-js', 'vendor-css' /* etc ... */]);
Your vendor-js task could then look like this:
var glob = require('glob');
gulp.task('vendor-js', function () {
var js = glob.sync('../vendor/**/*.js');
if (isProduction) {
// use <file>.min.js, unless there is only <file>.js
js = js.filter(function(file) {
return file.match(/\.min\.js$/) ||
js.indexOf(file.replace(/\.js$/, '.min.js')) < 0;
});
} else {
// use <file>.js, unless there is only <file>.min.js
js = js.filter(function(file) {
return !file.match(/\.min\.js$/) ||
js.indexOf(file.replace(/\.min\.js$/, '.js')) < 0;
});
}
gulp.src(js, { base: '../vendor' })
.pipe($.if(isProduction, // only minify for prod and when
$.if("!**/*.min.js", uglify()))) // the file isn't minified already
.pipe(gulp.dest('build'));
});
Adapting this to you specific needs should be fairly trivial from here on.

grunt task src filtering from cli

I have a grunt task that runs all of my nodejs server side test files.
grunt.registerTask('serverTest', ['env:test', 'mongoose', 'mochaTest']);
While developing tests I would like to only run the test I'm currently working on. My task is setup to run a list of all test assets in the code base:
mochaTest: {
src: testAssets.tests.server,
options: {
reporter: 'spec'
}
},
I would like to specify a filter parameter at the cli to only select a specific file from testAssets.tests.server which is a big list of tests since it's defined as modules/*/tests/server/**/*.js and returns every test in the code base.
For example if I entered the following at the command line:
grunt serverTest:*fileUnderTest.js
Grunt would filter the testAssets.tests.server and only execute the files that match *fileUnderTest.js
I've read through the documentation and seen a lot of examples but can't figure out how to do this without creating a separate target and specifying the file name in the grunt file.
There's probably a more efficient means but the following code at the start of my gruntfile got the filtering working for me.
// get regex epression from CLI
var expression = grunt.option('filter');
//filter the testAssets.tests.server array
var testFiles = filtered(getGlobbedPaths(testAssets.tests.server),expression);
// test array with regex and return array of matches
var filtered = (function(arr,pattern){
var filtered = [],
i = arr.length,
re = new RegExp(pattern);
while (i--) {
if (re.test(arr[i])) {
filtered.push(arr[i]);
}
}
return filtered;
});
And then the grunt task becomes:
mochaTest: {
src: testFiles,
options: {
reporter: 'spec'
}
}
grunt.registerTask('serverTest', ['env:test', 'mongoose', 'mochaTest']);
So a command line call such as:
grunt serverTest --filter=comment.server.model.tests.js
Will run any test file matching the filter and a command like:
grunt serverTest
Will run all test files in the project
For reference my project is based on meanjs which globs all the paths to files so I run the getGlobbedPaths() function which is defined in config.js to first expand the file path array prior to filtering:
var getGlobbedPaths = function(globPatterns, excludes) {
// URL paths regex
var urlRegex = new RegExp('^(?:[a-z]+:)?\/\/', 'i');
// The output array
var output = [];
// If glob pattern is array so we use each pattern in a recursive way, otherwise we use glob
if (_.isArray(globPatterns)) {
globPatterns.forEach(function(globPattern) {
output = _.union(output, getGlobbedPaths(globPattern, excludes));
});
} else if (_.isString(globPatterns)) {
if (urlRegex.test(globPatterns)) {
output.push(globPatterns);
} else {
glob(globPatterns, {
sync: true
}, function(err, files) {
if (excludes) {
files = files.map(function(file) {
if (_.isArray(excludes)) {
for (var i in excludes) {
file = file.replace(excludes[i], '');
}
} else {
file = file.replace(excludes, '');
}
return file;
});
}
output = _.union(output, files);
});
}
}
return output;
};

jasmine with gulp, run tests on test file changed

Hi what I trying to do is to make watcher task with gulp which will run my jasmine tests. What I have done so far:
var watch = require("gulp-watch");
var jasmine = require("gulp-jasmine");
gulp.task('tests.run.change-watcher', function (cb) {
gulp.src(testsFiles)
.pipe(watch(testsFiles))
.pipe(jasmine({ verbose: true }));
});
But when I run that task and try to change any file which meets the testsFiles rules it doesn't show anything in console.
However when I run the next task:
gulp.task('tests.run', function (cb) {
gulp.src(testsFiles)
.pipe(jasmine({verbose:true}));
});
It works and shows next:
8 specs, 0 failures Finished in 0 seconds
Maybe I miss something?
Do it in two steps
1) Declare the test-unit task (like you did)
gulp.task('tests.run', function () {
return gulp.src(testsFiles)
.pipe(jasmine({verbose:true}));
});
2) Declare the watch task that will run this test-unit task when those testsFiles change
gulp.task('tests.watch', function () {
gulp.watch(testsFiles, ['tests.run']);
});
Then, you run gulp tests.watch
To run only needed specs, try something like this:
/** Watches file changes in source or spec files and executes specs automatically */
gulp.task("specs-watcher", function() {
return watch(["src/**/*.ts", "spec/**/*.ts"], { events: ["add", "change"] }, function(vinyl, event) {
if (!vinyl.isDirectory()) {
if (vinyl.basename.endsWith(".spec.ts")) {
// We are dealing with a spec file here, so call jasmine!
runJasmine(vinyl.path);
} else {
// Try to find out specs file
const specFilePath = findSpecsFile(vinyl);
if (typeof specFilePath === "string") {
runJasmine(specFilePath);
}
}
}
});
});
This watcher uses two functions, one is for deriving the spec name based on the file name. In my case, it's:
/**
* For your specs-watcher: This function is called every time a file changed which doesn't end with '.spec.ts'.
* The function's task is to return the fitting specs path of this file. For example by looking for a corresponding file in the "/spec/" folder.
* #param {vinyl} changedFile Vinyl object of changed file (see https://github.com/gulpjs/vinyl)
* #return {string|undefined} Path to the specs file to execute or undefined if your watcher shouldn't do anything.
*/
function findSpecsFile(changedFile) {
return changedFile.path.replace(__dirname, `${__dirname}/spec`).replace(".ts", ".spec.ts");
}
The other function is runJasmine, which runs jasmine with a given test file.
Just make everything fit to your setup and it should work. :-)
You can listen to file changes for both tests and source code folders with this:
"use strict";
var gulp = require('gulp');
var mocha = require('gulp-mocha');
var batch = require('gulp-batch');
gulp.watch(['tests/**', 'src/**'], batch(function (events, cb) {
return gulp.src(['tests/*.js'])
.pipe(jasmine({ verbose: true }))
.on('error', function (err) {
console.log(err.stack);
});
}));
gulp.task('default', () => {
console.log('Gulp is watching file changes...');
});

Brunch plugin pipline issues

I'm writing a plugin for Brunch to 'filter' files from code library. Basic idea is to:
check my source files (in src\ folder, or any watched folders that don't match library pattern),
build a list of imported/required modules from code library (in lib\ folder, outside src\, somewhere on disk)
check files against this list and 'approve' or 'reject' them
compile only what's 'approved', so I don't end up with huge files that have all modules/components from my library, but only what I use in particular project
When I work only with JavaScript files this.pattern = /.*(js|jsx)$/; everything works fine. Next step is to include more files, since many modules/components in library have some sort of template or stylesheets, for example this is one AngularJS module:
lib\
modules\
pager\
controller.jsx
directive.jsx
template.html
pager.styl
README.md
But when I expand the pattern to include other files this.pattern = /.*/;, I run into all sorts of issues (; Most have to do with pipline - those are the kinds of errors I'm getting. For example:
jshint-brunch doesn't like README.md
html-brunch won't wrap template.html
stylus-brunch and sass-brunch are also unhappy
I've tried solving these problems individually, for example if I disable html-brunch config.plugins.off: ['html-brunch'], and add this code inside the compiler function, it kinda works:
if( params.path.match(/.html$/) ) {
params.data = "module.exports = function() { return " + JSON.stringify(params.data) + ";};";
return callback(null, this.config.modules.wrapper(params.path, params.data));
}
..but I couldn't resolve all the issues. Pretty much all problems have to do with this line in the compiler function: return callback(null, null);. When I 'reject' a file next plugin gets something undefined and breaks...
Any ideas how to solve this?
I'd like to eventually expand plugin's functionality to handle static assets too, for example copy lib\images\placeholder-1.jpg (but not placeholder-2.jpg) from library if it's used in html files, but I'm stuck at this point...
Here's the code of the plugin:
var CodeLibrary;
module.exports = CodeLibrary = (function() {
var required = [];
CodeLibrary.prototype.brunchPlugin = true;
function CodeLibrary(config) {
this.config = config;
this.pattern = /.*/;
this.watched = this.config.paths.watched.filter(function(path) {
return !path.match( config.plugins.library.pattern );
});
}
function is_required(path) {
var name = this.config.modules.nameCleaner(path);
return required.some(function(e, i, a) { return name.match(e); });
}
function in_library(path) {
return Boolean(path.match( this.config.plugins.library.pattern ));
}
function is_watched(path) {
return this.watched.some(function(e, i, a) { return path.match( e ); });
}
CodeLibrary.prototype.lint = function(data, path, callback) {
if( !is_watched.apply(this, [path]) &&
!is_required.apply(this, [path]) )
return callback();
var es6_pattern = /import .*'(.*)'/gm;
var commonjs_pattern = /require\('(.*)'\)/gm;
var match = es6_pattern.exec(data) || commonjs_pattern.exec(data);
while( match != null ) {
if( required.indexOf(match[1]) === -1 )
required.push( match[1] );
match = es6_pattern.exec(data) || commonjs_pattern.exec(data);
}
callback();
}
CodeLibrary.prototype.compile = function(params, callback) {
if( is_required.apply(this, [params.path]) ||
!in_library.apply(this, [params.path]) )
return callback(null, params);
return callback(null, null);
};
return CodeLibrary;
})();

Resources