Grunt cssmin / CleanCSS source map rebasing - gruntjs

I'm using cssmin with the following "Content" folder structure:
src
|--dir1
| |--style1.css
| |--images
| |--image1.png
|--dir2
|--style2.css
|--images
|--image2.png
dist
|--styles.min.css
|--styles.min.css.map
Where styles.min.css and styles.min.css.map are the result of concatenating/minifying all stylesheets in the"src" folder.
I first had issues where styles.min.css contained URLs for images in the wrong places (i.e. "images/image1.png" instead of "../src/dir1/images/image1.png") but thankfully this grunt configuration fixed that:
cssmin: {
options: {
rebase: true,
sourceMap: true
},
all: {
options: {
keepSpecialComments: 0
},
files: {
'content/dist/styles.min.css': ["content/src/dir1/style1.css", "content/src/dir2/style2.css"]
}
}
}
The new problem: The generated sourcemap ("styles.min.css.map") contains sources like this: ["content/src/dir1/style1.css", "content/src/dir2/style2.css"] instead of ["../src/dir1/style1.css", "../src/dir2/style2.css"]. This means the map is pointing to the incorrect locations, such as:
"content/dist/content/src/dir1/style1.css" and "content/dist/content/src/dir2/style2.css"
What can I do to resolve this?
For reference, I have also tried the csswring, however despite sourcemaps working fine, I found general image/import url rebasing wasn't working properly, so went back to cssmin.
Thanks very much!

Came up with my own solution. I wrote a task which reads the source map JSON, gets the array of sources, rebases them, then writes the file again. This solution seems to work well for me, hopefully this can help someone else too if they're in a similar situation. Just run your cssmin task and then this one:
grunt.registerTask("rebase-css-sourcemap-sources", "Rebases the CSS source map urls", function() {
var filePath = "./content/dist/styles.min.css.map";
if (grunt.file.exists(filePath)) {
var sourceMap = grunt.file.readJSON(filePath);
var sources = sourceMap.sources;
if (sources) {
for (var i = 0; i < sources.length; i++) {
sources[i] = sources[i].replace("content/src", "../src");
}
grunt.file.write(filePath, JSON.stringify(sourceMap));
grunt.log.ok("Rebased CSS source map source urls.");
}
} else {
grunt.log.error("Source map file does not exist: " + filePath);
}
});
While this solution works for me, if anyone knows of an alternative method of solving this problem which ideally just uses cssmin, that would be better.

Related

Replace Google Fonts with self hosted fonts

I'm currently porting parts of a legacy codebase which has more than 100 themes that each come with their own css files. Those files are full with hardcoded links to Google Fonts which need to be replaced due to GDPR.
Is there some kind of automated tool available which scans through these files, replaces the link to Google Fonts and downloads all the assets? I've found a couple of semi-automated tools online but they all require copy & paste and manual download of the files. That's okay for 2-3 fonts but not for hundreds of them. Any tips for that?
I have put some efforts to create this NodeJS script.
This script searches for all css files and extracts the woff font url. Then, it replaces it with absolute path of the downloaded file against the url it found, also downloads the file in the relevant directory which can be clearly identified in the snippet as specified with fontDownloadDirectoryPath variable.
This script can be modified and improved further but as of now, provides the required functionality at its base level.
I hope this can serve as a starting point atleast to solve the stated problem or can be used completely as a solution changes few variables, given that my assumptions of few required things to get to this solution are correct.
Please feel free to modify, accordingly like the regex pattern to match something else, adding few other font types in the pattern, adding few more code to make it more robust and generalised, etc for other possibilities.
const path = require('path');
const fs = require('fs');
const https = require("https");
// update assets/css with your css path
const directoryPath = path.join(__dirname, 'assets/css');
let fontDownloadDirectoryPath = path.join(__dirname, 'assets/fonts')
let fontDownloadDirectoryFileFullPath = path.join(__dirname, 'assets/fonts/fontsDownloadUrlList.json')
fs.readdir(directoryPath, function (err, files) {
//handling error
if (err) {
return console.log('Unable to scan directory: ' + err);
}
//listing all files using forEach
files.forEach(function (file) {
// Do whatever you want to do with the file
let file_full_path = directoryPath + "/" + file
fs.readFile(file_full_path, 'utf8', (err, content) => {
if (err) {
console.error(err);
return;
}
// console.log(content);// show the content of readed file
let found = content.match(/url\(['"]([^"']+(woff2|eot|woff|ttf)["'])+\)/gi)
console.log(file_full_path, found);
let updated_content = content
if (found) {
if (fs.existsSync(fontDownloadDirectoryFileFullPath)) {
// enter the code to execute after the folder is there.
console.log('file exists')
fs.readFile(fontDownloadDirectoryFileFullPath, 'utf8', (err, read_content) => {
let read_content_json = JSON.parse(read_content)
read_content_json.push(...found)
fs.writeFile(fontDownloadDirectoryFileFullPath, JSON.stringify(read_content_json), function () { })
})
} else {
fs.writeFile(fontDownloadDirectoryFileFullPath, JSON.stringify(found), function () { })
}
console.log(found)
found.forEach((item) => {
let fontFileUrl = item.split("'")[1]
let fontFileName = fontFileUrl.split("/")[fontFileUrl.split("/").length - 1]
console.log(fontFileUrl, fontFileName)
https.get(fontFileUrl, response => {
var body = '';
var i = 0;
response.on('data', function (chunk) {
i++;
body += chunk;
console.log('BODY Part: ' + i);
});
response.on('end', function () {
console.log(body);
fs.writeFileSync(fontDownloadDirectoryPath + "/" + fontFileName, body, { encoding: 'utf8', flag: 'w' }, (err) => { console.log(err) })
console.log('Finished');
});
});
updated_content = updated_content.replace(item, "url('" + fontDownloadDirectoryPath + "/" + fontFileName + "')")
})
} else {
updated_content = content;
}
fs.writeFileSync(file_full_path, updated_content, { encoding: 'utf8', flag: 'w' })
});
});
});
I used below css file in root/assets/css directory with styles.css name for testing the above script:
#font-face {
font-family: 'BR Firma';
src: url('https://fonts.gstatic.com/s/opensans/v29/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsiH0B4taVQUwaEQbjB_mQ.woff') format('woff');
font-weight: bold;
font-style: normal;
font-display: swap;
}
without having more background on the project, directory structure, and so on, I will outline how the task could be done as of now:
Scan all the directories or URLs of the project (if you run it locally or remotely) if the fonts are not being imported from one main CSS file (could happen).
Get all the Google Fonts URLs
Download all the assets (i.e. fonts from the links, maybe some pics also, etc.)
So, although you can totally do this locally with directories, here I will explain a way to do it with the browser for brevity - and possibly convenience - with Python. I am assuming you have access to the project's URLs, ofc.
You can follow this approach to scrape the URLs you want. Pass it a list from the sitemap to go through all the URLs in sequence. Then you can filter the list you get to account only for Google Fonts, simply add the in operator (as in here) to get true or false, respectively.
substring = 'https://fonts.googleapis.com'
if substring in element:
list.append(element)
else:
#do nothing, don't add it to list
Now you should have all the URLs you are interested in. If the project has several HTML pages with different fonts, those are the ones you need to scan - or maybe just all of them to be sure. Note: it is usually useful to store the list in a file to further add this code to the previous script. More info.
with open('urls.txt', 'w') as f:
f.write(element)
To download the assets, you can use this approach. Since you have all the URLs, you should be able to.
And that's pretty much it! If you add more insight into the project structure we could complete the scripts more precisely. Also, you can quickly use Jupyter Notebook to run the scripts as you tune them. In the meantime, the open details to clarify would be:
Scan websites or files?
Only HTML files or all the projects?
What to download? The font assets only?
Python script works fine for this task?
Any IDE can do, just "search and replace in files", with the appropriate patterns.
For example: PHPStorm: Find and replace text using regular expressions. Finding all the occurrences alone is already worth something and an IDE might help with "porting parts of a legacy codebase".

Gulp Task: CleanCSS and Paths

I've created a task with Gulp that is supposed to:
Join multiple CSS files;
Minify + remove unnecessary CSS;
Fix paths for url() directives and others;
Generate source maps;
My current code for this is:
var gulp = require("gulp"),
concat = require("gulp-concat"),
cleanCSS = require("gulp-clean-css"),
sourcemaps = require("gulp-sourcemaps");
var styleList = [
"Resources/Include/ionicons/css/ionicons.css",
"Resources/base.css",
"Resources/extra.css",
];
gulp.task("deploy-css", function() {
gulp.src(styleList)
.pipe(sourcemaps.init())
.pipe(concat("style.min.css"))
.pipe(cleanCSS({
debug: true,
compatibility: "ie8",
keepSpecialComments : 0,
target: "Resources/",
relativeTo: "Resources/"
})
)
.pipe(sourcemaps.write())
.pipe(gulp.dest("Resources/"))
});
url() path example, taken from file Resources/Include/ionicons/css/ionicons.css:
#font-face { font-family: "Ionicons"; src: url("../fonts/ionicons.eot?v=2.0.0");
This is my current file structure:
./Resources/style.min.css // -> Final processed file
./Resources/base.css
./Resources/extras.css
./Resources/Include/ // -> Original CSS files with URL (installed via Bower)
Test folder: https://dl.dropboxusercontent.com/u/2333896/gulp-path-test.zip (install and then run with gulp deploy-css).
Almost everything works as expected, except for when CSS files include references to images or fonts using the url() option. After running the task (and style.min.css created) those references are broken - no change was made to the paths found on the original CSS files.
Isn't cleanCSS supposed to check where the referenced files are and fix the paths automatically? Aren't the options target and relativeTo used to control that?
How can I fix this? Thank you.
I managed to fix the issue, my main problems were a misplaced concat operation breaking gulp-clean-css rebase feature and wrong target and relativeTo options. Apparently I didn't think much about the previous workflow.
var gulp = require("gulp"),
concat = require("gulp-concat"),
cleanCSS = require("gulp-clean-css"),
sourcemaps = require("gulp-sourcemaps");
var styleList = [
"Resources/Include/ionicons/css/ionicons.css",
"Resources/base.css",
"Resources/extra.css",
"Resources/Include/teste/base.css"
];
gulp.task("deploy-css", function() {
gulp.src(styleList)
.pipe(sourcemaps.init())
.pipe(cleanCSS({
compatibility: "ie8",
keepSpecialComments : 0,
target: "Resources",
relativeTo: ""
})
)
.pipe(concat("style.min.css", {newLine: ""}))
.pipe(sourcemaps.write())
.pipe(gulp.dest("Resources"))
});
This new workflow works as:
Optimize all individual CSS files - including rebasing urls;
Contact individual optimized files into the final file - (note newLine: "" avoids line breaks in the file);
Write the file.

Grunt-complexity on all the files in a directory

I'd like to run Grunt-Complexity on all the files in a directory?
I'd like to get this kind of output.
Is there a way?
My js files are all under a subdirectory called "js".
Here's my gruntfile:
/*global module:false*/
module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
// Task configuration.
complexity: {
generic: {
src: ['grunt.js', 'js/*'],
//exclude: ['doNotTest.js'],
options: {
breakOnErrors: false,
jsLintXML: 'report.xml', // create XML JSLint-like report
checkstyleXML: 'checkstyle.xml', // create checkstyle report
pmdXML: 'pmd.xml', // create pmd report
errorsOnly: false, // show only maintainability errors
cyclomatic: [3, 7, 12], // or optionally a single value, like 3
halstead: [8, 13, 20], // or optionally a single value, like 8
maintainability: 100,
hideComplexFunctions: false, // only display maintainability
broadcast: false // broadcast data over event-bus
}
}
}
});
// These plugins provide necessary tasks.
grunt.loadNpmTasks('grunt-complexity');
// Default task.
grunt.registerTask('default', 'complexity');
};
I'm simply calling this by typing
grunt
from the command line.
then if I type this
grunt complexity js/*
I get
Warning: Task "js/AgencyMediaController.js" not found. Use --force to continue.
Aborted due to warnings.
And AgencyMediaController.js is the first file in my js directory. So it's having a look and listing the files, but then it crashes.
Thanx!
example:
for all js file in JS folder:
src: ['js/**/*.js']
for ass .scss files in scss folder:
src: ['scss/**/*.scss']
I suggest for you create a config for your src folder can be easy in future folder changes in future projects:
sample:
var src;
config.src = src = {
sassMain : 'scss/main.scss',
distFolder : 'public/stylesheets/lovelycss.dist.css',
devFolder : 'public/stylesheets/lovelycss.dev.css',
libFolder : 'lib/**/*.js',
sassFolder : 'scss/**/*.scss',
spriteCssFolder : 'scss/helpers/_sprite.scss',
spriteDestImg : 'public/images/sprite/spritesheet.png',
spriteSrc : 'public/images/min/*.{png,jpg,gif}',
imageminCwd : 'public/images/',
imageminDest : 'public/images/min'
};
//grunt Watch ===============================
config.watch = {
scripts: {
files: ["<%= src.libFolder %>", "<%= src.sassFolder %>"]
,tasks: ["dev", "sass:dist"]
//,tasks: ["dev",'sass:dist']
}
}
I hope that helped you.
It's been quite a long while since I asked this question. I just ran into the same issue again and found the answer so here it is:
In the end it turned out to be that one of the files I was trying to analyse was causing the crash. This particular Javascript environment allows for C-like preprocessor directives and the Javascript file had something like this:
var mySettings = {
//#ifdef FOO_CONSTANT
setting : constants.FOO_SETTING
//#endif
//#ifdef BAR_CONSTANT
setting : constants.BAR_SETTING
//#endif
};
I guess the problem is that if this is read as strictly Javascript, the preprocessor directives are just plane comments, and there's a comma missing between the two properties, so Grunt complexity is unable to read this because of a syntax error. Using --force makes no difference BTW.
The annoying part is that this is all the error shows:
$ grunt --force
Running "complexity:generic" (complexity) task
Warning: undefined: Unexpected token, expected , (17570:1) Used --force, continuing.
Done, but with warnings.
So while it does say expected , (175:1) it doesn't say in which of the several Javascript files in this project the problem was found!
Just adding exclude: ['path/to/MyFileWithPreprocessorDirectives.js'] to Gruntfile.js in order to exclude this file from the analysis gets me around the problem.

Change format of the Source map created from closure compiler

I have this closure-compiler task defined with following options:
'closure-compiler': {
files: {
},
options: {
externs: [],
compilation_level: 'ADVANCED_OPTIMIZATIONS',
language_in: 'ECMASCRIPT5_STRICT',
create_source_map: '<%= sourceDir %>js/<%= outputName %>.min.js.map',
output_wrapper: '%output%\n//# sourceMappingURL=<%= sourceMapURL %>js/<%= outputName %>.min.js.map'
}
}
the sourcemap is created and it looks like this:
{
"version":3,
"file":"build/js/game.min.js",
"lineCount":39,
"mappings":"AAEA,...",
"sources":["/src/js/utils.js","/src/js/game/Button.js",...],
"names":[...]
}
but then the source map doesnt work, what I need is:
{
"version":3,
"file":"game.min.js",
"lineCount":39,
"mappings":"AAEA,...",
"sources":["utils.js","game/Button.js",...],
"names":[...]
}
what should I do to have the sourcemap created in that form?
For Grunt, there are many options for sourcemaps that have to be handled as a separate build step. It lacks the power of the gulp-sourcemaps plugin and so either each tool has to handle every conceivable option for generating a sourcemap or another tool must be used.
Post processing a sourcemap in this fashion isn't too difficult as sourcemaps are JSON data.
grunt-sourcemap-localize looks to do exactly what you are wanting.

How do I process multiple files as templates with yeoman generator?

I'm working on a custom generator that I originally wrote with grunt-init. One difference I'm noticing is grunt-init automatically processes all the files in root as templates but with yeoman generator you have to do this using .template(). I'm familiar with how to process any individual file with .template() but is it possible to process an entire directory?
This issue is an annoying one. I came across this when I used yeoman for the first time. I think the below code snippet can help you.
this.directory('scripts', 'scripts');// script is folder name
Looking at the Yeoman generator code, there doesn't seem to be a built in way to do this. The way I solved this was to copy some of the code from the built in generator code and modify it for my needs. I determine if a file is a template based on the _ prefix convention since I want to rename the files to exclude that prefix, but you could just treat every file as a template and it would work fine. This will copy all of the files in the directory, so what I also did is to exclude the .DS_STORE files that you find by default on OSX, but since that is a specific case I didn't include that here.
require('path');
MyGenerator.prototype._processDirectory = function(source, destination) {
var root = this.isPathAbsolute(source) ? source : path.join(this.sourceRoot(), source);
var files = this.expandFiles('**', { dot: true, cwd: root });
for (var i = 0; i < files.length; i++) {
var f = files[i];
var src = path.join(root, f);
if(path.basename(f).indexOf('_') == 0){
var dest = path.join(destination, path.dirname(f), path.basename(f).replace(/^_/, ''));
this.template(src, dest);
}
else{
var dest = path.join(destination, f);
this.copy(src, dest);
}
}
};
Yeoman uses mem-fs-editor, which has support for glob patterns. However the documentation is not very clear, and you may miss that point. Here is the documentation of copyTpl, that says it accepts the same options as copy. So since copy has suppport for glob patterns, copyTpl too.
At any point on your yeoman generator you can do:
const from = 'myFolder/**.js'
const to = 'project/'
this
.fs
.copyTpl(
this.templatePath(from),
this.destinationPath(to),
this.props, {interpolate: /<%=([\s\S]+?)%>/g}
);
},
Not that, if you are using a glob pattern the destination path should be a folder.

Resources