Keep Grunt file configs maintainable with expand, cwd, and rename
Replace hardcoded Grunt file lists with dynamic globs, cwd resolution, and rename transforms for maintainable builds.
15 Mar 2026, 15:00 UTC

Stop maintaining a manual file list in Grunt
A Gruntfile often begins as a short list of source and destination files. Six months later, that list has grown, someone added a comment that says don't forget to add new files here, and a production deploy missed a module because a developer created src/components/new-widget.js but never updated the build config.
The useful takeaway is that Grunt's expand: true file object lets you declare sources with globs, resolve them relative to a cwd, and rename destinations with a function. The build configuration then follows the repository structure instead of requiring a manual edit every time a file is added or removed.
What expand: true actually does
In a Grunt multi-task such as grunt-contrib-copy, a basic files entry maps one source to one destination. Setting expand: true switches Grunt into dynamic mapping mode: it resolves the src globs relative to cwd, then generates a source-to-destination pair for every match. The rename function receives each matched path and returns the destination path, so you can strip prefixes, change extensions, or flatten folders without an external script.
This convention appears in grunt-contrib-copy, grunt-contrib-uglify, and grunt-contrib-concat, so the same pattern works across tasks.
Worked example: copy assets without editing the Gruntfile
Suppose your project keeps source assets under src/assets and publishes them to dist/assets. Instead of listing every file, configure the copy task like this:
module.exports = function(grunt) {
grunt.initConfig({
copy: {
assets: {
expand: true,
cwd: 'src/assets',
src: ['**/*.{js,css,png}'],
dest: 'dist/assets',
rename: function(dest, src) {
return dest + '/' + src;
}
}
}
});
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.registerTask('default', ['copy:assets']);
};
Run this from the project root, where the Gruntfile lives. You need read access to src/assets and write access to dist/assets:
grunt copy:assets --verbose
The --verbose flag is the diagnostic check: Grunt should print each expanded source and destination pair before it copies anything. If a new file appears under src/assets, the next run picks it up with no Gruntfile change.
A glob such as **/* can match build output or node_modules if cwd is set too high. Keep cwd narrow, or add an ignore array.
Guardrails: filter, ignore, and evaluation timing
Grunt resolves the file list when the configuration loads, not lazily while the task runs. That makes logging predictable, but it also means files created after Grunt starts are not included in that run.
Use filter: 'isFile' to skip directories matched by a glob. If rename returns null or undefined, Grunt silently skips that file, so a typo in the transform can drop assets without an error.
A safer copy configuration might look like this:
copy: {
assets: {
expand: true,
cwd: 'src/assets',
src: ['**/*.{js,css}'],
dest: 'dist/assets',
filter: 'isFile',
ignore: ['**/node_modules/**', '**/*.tmp'],
rename: function(dest, src) {
if (!src) return undefined;
return dest + '/' + src;
}
}
}
Trade-offs and how to verify the result
The main trade-off is explicitness versus convenience. Dynamic globs hide the exact file list from a quick scan of the Gruntfile, which can make it harder to audit what will be deployed.
Practical verification:
- Run
grunt copy:assets --verboseand confirm the logged pairs match your expectation. - Add a temporary file under
src/assets, run the task again, and confirm it appears indist/assets. - Add an intentionally unmatched glob such as
src/assets/does-not-exist.jsand confirm the task does not fail.
If the verbose output shows unexpected destinations, tighten cwd or src before running the build in CI.
Closing
Hardcoded file lists scale poorly. With expand: true, cwd, and rename, your Grunt configuration tracks the repository structure instead of duplicating it. Pair the pattern with --verbose logging and narrow globs so the build stays maintainable as the project grows.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.