Reputation: 33
I am attempting to remove all unused image links within multiple subdirectories using grunt-unused. Here is my folder structure for clarity:
|-- dist
| |-- site-1
| | |—-index.html
| | |—-style.css
| | |—-app.js
| | |—-image1.jpg
| | |—-image2.jpg
| | |—-image3.jpg
| | |—-image4.jpg
| |-- site-2
| | |—-index.html
| | |—-style.css
| | |—-app.js
| | |—-image1.jpg
| | |—-image2.jpg
| | |—-image3.jpg
| | |—-image4.jpg
| |-- site-3
| | |—-index.html
| | |—-style.css
| | |—-app.js
| | |—-image1.jpg
| | |—-image2.jpg
| | |—-image3.jpg
| | |—-image4.jpg
I have wrote a 'forEach' function to target these child folders and then reference each folder, similar to this post but it is not working for the unused task. It does not loop through each directory and console detects no unused files in a very quick time, like it is not actually doing anything.
Where am I going wrong?
grunt.registerTask('prodOutput', function () {
// read all subdirectories of dist/ folder, excluding the assets folder
grunt.file.expand('dist/*', '!dist/assets/**').forEach(function (dir) {
//get the current unused config
var unusedConfig = grunt.config.get('unused') || {};
//set options for grunt-unused, including the folder variable 'dir' as the reference of where to look
unusedConfig[dir] = {
option: {
reference: dir + '/',
directory: ['**/*.css', '**/*.html', '**/*.js'],
remove: false, // set to true to delete unused files from project
reportOutput: 'report.txt', // set to false to disable file output
fail: false // set to true to make the task fail when unused files are found
}
};
grunt.config.set('unused', unusedConfig);
});
grunt.task.run('unused');
});
Upvotes: 3
Views: 917
Reputation: 24992
As mentioned earlier in my comment... The main issue is that you are dynamically configuring multiple Targets in your forEach
Function prior to running the task, however grunt-unused does not support multiple target configurations.
Also grunt-unused
expects the files that include references/links to other files (i.e. index.html
) to be in a different directory/folder than the files it references (e.g. images, css, etc), however the folder structure provided in your post is flattened.
After a quick look around at other grunt plugins there doesn't seem to be one that will meet your requirement. The only way I think you can achieve this is to write your own custom Task/plugin to handle this.
To achieve this you could do the following:
Create a separate Javascript module that exports a Registered MutliTask. Name the file delete-unused.js
and save it to a directory named tasks
which resides in the same top level directory as your Gruntfile.js
.
Directory structure
Your directory structure should be something like this:
.
├── dist
│ ├── site-1
│ ├── site-2
│ ├── ...
│ └── assets
│
├── Gruntfile.js
│
├── node_modules
│ └── ...
│
├── package.json
│
└── tasks
└── delete-unused.js
delete-unused.js
module.exports = function(grunt) {
'use strict';
// Requirements
var fs = require('fs');
var path = require('path');
grunt.registerMultiTask('unused', 'Delete unused assets', function() {
// Default options. These are used when no options are
// provided via the initConfig({...}) papaparse task.
var options = this.options({
reportOutput: false,
remove: false
});
var reportTxt = '';
// Loop over each src directory path provided via the configs src array.
this.data.src.forEach(function(dir) {
// Log if the directory src path provided cannot be found.
if (!grunt.file.isDir(dir)) {
grunt.fail.warn('Directory not found: '.yellow + dir.yellow);
}
// Append a forward slash If the directory path provided
// in the src Array does not end with one.
if (dir.slice(-1) !== '/') {
dir += '/';
}
// Generate the globbin pattern (only one level deep !).
var glob = [dir, '*'].join('');
// Create an Array of all top level folders (e.g. site-*)
// in the dist directory and exclude the assets directory.
var dirs = grunt.file.expand(glob).map(function(dir) {
return dir;
});
// Loop over each directory.
dirs.forEach(function(dir) {
// Add the folders to exclude here.
if (dir === './dist/assets') {
return;
}
// Log status and update report
grunt.log.write('\nProcessing folder ' + dir);
reportTxt += '\nProcessing folder ' + dir;
// Empty Array to be populated with unused asset references.
var unused = [];
// Define the path to index.html
var pathToHtml = [dir, '/', 'index.html'].join('');
// Create Array of file names and filepaths (exclude index.html)
var assets = grunt.file.expand([dir + '/**/*.*', '!index.html'])
.map(function(file) {
return {
fpath: file,
fname: path.basename(file)
};
});
// Log/Report missing 'index.html' and return early.
if (!grunt.file.exists(pathToHtml)) {
grunt.log.write('\n >> Cannot find index.html in ' + dir + '/\n');
reportTxt += '\n >> Cannot find index.html in ' + dir + '/\n';
return;
}
// Read the contents of index.html.
var html = fs.readFileSync(pathToHtml, {
encoding: 'utf8'
});
// Loop over each file asset to find if linked in index.html
assets.forEach(function(asset) {
// Backslash-escape the dot [.] in filename for RegExp object.
var escapedFilename = asset.fname.replace('.', '\\.');
// Dynamically create a RegExp object to search for instances
// of the asset filename in the contents of index.html.
// This ensures the reference is an actual linked asset and
// not plain text written elsewhere in the document.
//
// For an explanation of this Regular Expression visit:
// https://regex101.com/r/XZpldm/4/
var regex = new RegExp("(?:href=|src=|url\\()(?:[\",']?)(.*"
+ escapedFilename + ")[\",',\\)]+?", "g");
// Search index.html using the regex
if (html.search(regex) === -1 && asset.fname !== 'index.html') {
unused.push(asset); // <-- Not found so add to list.
}
});
// Log status and update report
grunt.log.write('\n ' + unused.length + ' unused assets found:\n');
reportTxt += '\n ' + unused.length + ' unused assets found:\n';
//Delete the unused asset files.
unused.forEach(function(asset) {
if (options.remove) {
grunt.file.delete(asset.fpath);
// Log status and update report
grunt.log.write(' deleted: ' + asset.fpath + '\n');
reportTxt += ' deleted: ' + asset.fpath + '\n';
} else {
// Log status and update report
grunt.log.write(' ' + asset.fpath + '\n');
reportTxt += ' ' + asset.fpath + '\n';
}
});
});
if (options.reportOutput) {
grunt.file.write(options.reportOutput, reportTxt);
}
});
});
};
Gruntfile.js
Configure your Gruntfile.js
as follows.
module.exports = function(grunt) {
'use strict';
grunt.initConfig({
// ...
unused: {
options: {
reportOutput: 'report.txt',
remove: true
},
dist: {
src: ['./dist/']
}
}
});
// Load the custom multiTask named `unused` - which is defined
// in `delete-unused.js` stored in the directory named `tasks`.
grunt.loadTasks('./tasks');
// Register and add unused to the default Task.
grunt.registerTask('default', [
// ...
'unused',
// ...
]);
// Or add it to another named Task.
grunt.registerTask('foobar', [
// ...
'unused',
// ...
]);
};
Notes
The grunt-unused
plugin is no longer used, so you can uninstall it by running the following via your CLi tool: $ npm un -D grunt-unused
The delete-unused.js
custom module provides a similar configuration in Gruntfile.js
with less options than those found in grunt-unused
. The src
Array in the config accepts a path(s) to the folder to be processed (i.e. ./dist/
). and not a glob pattern - the glob pattern is generated inside grunt-unused.js
.
The default options for reportOutput
and remove
are set to false
in grunt-unused.js
. When you first run the Task I recommend that you initially set the remove
option to false
in your Gruntfile.js
configuration. This will simply log to the console the unused assets and allow you to check whether it meets your requirement. Obviously setting the remove
option to true
will delete any unused assets when it's re-run.
I noticed that in your post you provided the glob pattern '!dist/assets/**'
to exclude the assets
folder from being processed. Instead of passing the glob pattern to exclude via the src
configuration this has been hard-coded in delete-unused.js
. You'll see it in the lines that read:
// Add the folders to exclude here.
if (dir === './dist/assets') {
return;
}
If there are additional directories inside the dist
folder that you want to exclude you'll need to add them there: For example:
// Add the folders to exclude here.
if (dir === './dist/assets'
|| dir === './dist/foo'
|| dir === './dist/quux') {
return;
}
This solution checks whether the assets found inside the site-*
folders are linked/referenced inside the corresponding index.html
only and does not check whether they are referenced in any .css
file.
delete-unused.js
utilizes a Regex to find whether the asset is actually linked to the index.html
and is not as plain text written elsewhere in the document (inside a paragraph of text for example). An explanation of the custom Regular Expression used can be found here.
I hope this helps !
Upvotes: 1