Use Grunt task variables in config - gruntjs

I'd like to define variables in the config of my Grunt task, which are being replaced by my task.
My use-case is that I'm trying to create separate JS output files, based on a config.json which contains configurations for multiple sites.
An example of my config:
extractProjectConfigs: {
options: {
merge: '<%= yeoman.dist %>/extracted.js',
configWrapper: 'window.config = {{ extractedConfig }};'
},
prod: {
src: ['./config/*.json'],
dest: '.tmp/scripts/{{ configKey }}/searchbox.js'
}
}
In my extractProjectConfigs task, I define the variables configKey and extractedConfig and I'd like the values of both to be in my config.
How can I achieve this? I already tried including Grunt Templates in my config definition (i.e. <%= extractedConfig %> or <%= configKey %>), but since Grunt Template parses variables before passing them to my task, it basically means my variables turn into empty strings.

Turned out Grunt Templates were the right way to go. Grunt Templates parse variables defined by <%= %> before passing them to the task. However, Grunt Templates also offer a way to determine your own variable delimiter, which allows you to use Grunt Templates in your task that are parsed after the task ran, meaning I can use variables in my config.
I ended up using the following config:
extractProjectConfigs: {
options: {
merge: '<%= yeoman.dist %>/extracted.js',
configWrapper: 'window.config = {{= extractedConfig }};'
},
prod: {
src: ['./config/*.json'],
dest: '.tmp/scripts/{{= configKey }}/searchbox.js'
}
}
In my task, I used the following custom Grunt template function to parse the config:
function parseTemplate(template, vars) {
grunt.template.addDelimiters('double-brackets', '{{', '}}');
return grunt.template.process(template, {
data: vars,
delimiters: 'double-brackets'
});
}
This function can then be used within the task, for instance as follows:
var dest = parseTemplate(file.dest, {
projectKey: 'foo'
});
This can then be used within grunt.file.write: grunt.file.write(dest, 'write something to dest with projectKey replaced by foo');

Related

Grunt-string-replace using an object of key-value pairs

I started using grunt in my build process. In package.json I include variables to be replaced this way:
{
"name": "myApp",
"variableToReplace":{
"var1":"var1_replacement",
"var2":"var2_replacement"
},
...
}
However, I do not know how to use this variableToReplace in Gruntfile.js so that it will automatically look up all properties then replace them with the corresponding values. Is it possible using Grunt?
'string-replace': {
dist: {
files: {
//A list of files
},
options: {
//What to put here?
}
}
}
Revised Answer (after comment)
... Is there anyway to loop through the key-value pair of variableToReplace and replace, for example, var1 with var1_replacement.
Yes, this can be achieved by utilizing a Custom Task. The custom task should perform the following:
Read the variableToReplace object from package.json.
Dynamically build the options.replacements array.
Configure/set the option.replacements array using grunt.config.set
Then finally run the task using grunt.task.run.
The following Gruntfile.js demonstrates this solution:
Gruntfile.js
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-string-replace');
grunt.initConfig({
'string-replace': {
dist: {
files: [{
expand: true,
cwd: 'src/',
src: '**/*.css',
dest: 'dist/'
}],
options: {
replacements: [] // <-- Intentionally empty and will be dynamically
// configured via `configAndRunStringReplace`.
}
}
}
});
/**
* Helper task to dynamically configure the Array of Objects for the
* `options.replacements` property in the `dist` target of the `string-replace`
* task. Each property name of the `variableToReplace` Object (found in
* `package.json`) is set as the search string, and it's respective value
* is set as the replacement value.
*/
grunt.registerTask('configAndRunStringReplace', function () {
// 1. Read the `variableToReplace` object from `package.json`.
var replacements = grunt.file.readJSON('package.json').variableToReplace,
config = [];
// 2. Dynamically build the `options.replacements` array.
for (key in replacements) {
config.push({
pattern: new RegExp(key, 'g'),
replacement: replacements[key]
});
}
// 3. Configure the option.replacements values.
grunt.config.set('string-replace.dist.options.replacements', config);
// 4. Run the task.
grunt.task.run('string-replace:dist');
});
// Note: In the `default` Task we add the `configAndRunStringReplace`
// task to the taskList array instead of `string-replace`.
grunt.registerTask('default', ['configAndRunStringReplace']);
}
Important note regarding Regular Expressions:
The docs for grunt-string-replace states:
If the pattern is a string, only the first occurrence will be replaced...
To ensure that multiple instances of the search/find string are matched and replaced the configAndRunStringReplace custom task utilizes a Regular Expression with the global g flag.
So, any instances of the following regex special characters:
\ ^ $ * + ? . ( ) | { } [ ]
which may be used in a search word (i.e. as a key/property name in your package.json) will need to be escaped. The typical way to escape these characters in a Regex is to add a backslash \ before the character (E.g. \? or \+ etc..). However, because you're using key/property names in JSON to define your search word. You'll need to double escape any of the previously mentioned characters to ensure your JSON remains valid. For Example:
Lets say you wanted to replace question marks (?) with exclamation marks (!). Instead of defining those rules in package.json like this:
...
"variableToReplace":{
"?": "!"
},
...
Or like this:
...
"variableToReplace":{
"\?": "!"
},
...
You'll need to do this (i.e. use double escapes \\):
...
"variableToReplace":{
"\\?": "!"
},
...
Original Answer
The following contrived example shows how this can be achieved:
Gruntfile.js
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-string-replace');
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'), // 1. Read the package.json file.
'string-replace': {
dist: {
files: {
'dist/': ['src/css/**/*.css'],
},
options: {
replacements: [{
// 2. Use grunt templates to reference the properties in package.json
pattern: '<%= pkg.variableToReplace.var1 %>',
replacement: '<%= pkg.variableToReplace.var2 %>',
}]
}
}
}
});
grunt.registerTask('default', ['string-replace']);
}
Notes
Add pkg: grunt.file.readJSON('package.json') to the grunt.initConfig() section of your Gruntfile.js. This will parse the JSON data stored in package.json.
Use grunt templates to access the properties of package.json. The standard JavaScript dot notation is used to access the property values, (e.g. pkg.variableToReplace.var1), and is wrapped in a leading <%= and trailing %>
Using the contrived Gruntfile.js configuration above with your the package.json data (as described in your question). The following would occur:
Any instances of the string var1_replacement found in any of the .css files stored in the src/css/ directory will be replaced with the string var2_replacement.
The resultant files will be saved to the dist/ directory.

Grunt-string-replace not working

I have an app folder where I want to replace http://localhost:8000 for http://fomoapp-melbourne.rhcloud.com in two files: companies-list.component.ts and events-list.component.ts. I am trying to use grunt-replace-string plugin and it seemingly runs successfully with green Done result and no errors, but no replacement happens.
Here is how Gruntfile.js looks like:
module.exports = function(grunt){
[
'grunt-string-replace',
].forEach(function(task){
grunt.loadNpmTasks(task);
});
// configure plugins
grunt.initConfig({
'string-replace': {
dist: {
files: {
'./app/': ['companies-list.component.ts','events-list.component.ts'],
},
options: {
replacements: [{
pattern: 'http://localhost:8000',
replacement: 'http://fomoapp-melbourne.rhcloud.com',
}]
}
}
},
});
// register tasks
grunt.registerTask('default', ['string-replace']);
};
The Grunt Files object is for specifying a mapping of source files to destination files. The purpose of this mapping is to tell Grunt to get the contents of the files in source, do something to the contents, and then write the response to a new file in the destination folder.
It looks to me from your configuration that you want Grunt to rewrite two files in the app/ directory. This is not going to work. I will bet that if you run grunt with the verbose option, grunt --verbose, your output will contain the following:
Files: [no src] -> ./app/
This is because Grunt cannot find the source files because you need to specify their relative paths.
It's up to you how you want to structure your app, but you might want to have a src/ folder and a dist/ folder under app/. If you choose to build your files objects dynamically, your config might look something like this:
files: [{
expand: true,
cwd: './app/src/',
dest: './app/dest/',
src: ['companies-list.component.ts', 'events-list.component.ts']
}]
Additionally, the documentation for grunt-string-replace states:
If the pattern is a string, only the first occurrence will be replaced, as stated on String.prototype.replace.
This means that if you want multiple instances of your string to be replaced, you must provide a Regular Expression literal. For example:
replacements: [{
pattern: /http:\/\/localhost:8000/g,
replacement: 'http://fomoapp-melbourne.rhcloud.com'
}]

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.

Grunt convert markdown to pdf recursively and dynamically

I am stuck writing a Gruntfile which aim is to convert a bunch of Markdown files to PDF dynamically. Giving the current folder hierarchy:
root/
|_subfolder1
| |_filename1.md
|_subfolder2
|_filename2.md
...
|_node_modules
|_subfolderN
|filenameN.md
I would like to run a Markdown to PDF task which would process the md file and ouput a PDF file with the matching filename in the same output directory.
I did create a custom task which is parsing current directory, ignoring the mode_modules folder and get the markdown file, but I don't know how to configure the md2pdf task with the good properties to reflect dynamic folder mapping.
Here's my current Gruntfile:
module.exports = function(grunt) {
// 1 - Configuration
grunt.initConfig({
md2pdf: {
}
});
// 2 - Plugins
grunt.loadNpmTasks('grunt-md2pdf');
// 3 - Task registering
grunt.registerTask('default', 'Get Subfolders', function() {
grunt.file.recurse('.', callback);
function callback(abspath, rootdir, subdir, filename) {
var filenameOutExt;
// if current occurence is a file subdir == undefined
// checking subdir to true means it's not undefined and
// the current path is a directory
if(subdir) {
// excluding node_modules folder
if (!subdir.match('node_modules')) {
// only process markdown files
if(filename.match('.md')) {
filenameOutExt = filename.split('.')[0];
// now for each markdown files, run md2pdf task
// and ouput filenameOutExt.pdf in same folder
// as the input files
}
}
}
}
});
};
I am using this plugin: https://www.npmjs.com/package/grunt-md2pdf
So my questions is how should I configure the md2pdf task to pass it the markdown files and generate matching filename pdf output in same directory.
Output should be:
root/
|_subfolder1
| |_filename1.md
|_filename1.pdf
|_subfolder2
|_filename2.md
|_filename2.pdf
...
|_node_modules
|_subfolderN
|filenameN.md
|_filenameN.pdf
Thanks a lot
According to this line, this task uses the grunt.files utilitary function. This makes things easier for us!
First, it's not usual in grunt to create a different task to find the files you need in any other task.
That is, each task should receive the files it needs to operate on. For example...
coffee:
main:
files: [
expand: true
cwd: 'assets/script'
src: ['**/*.coffee']
dest: "assets/script"
ext: '.js'
]
(Note that this config is in a Gruntfile.coffee file, hence the CoffeeScript syntax)
This type of file configuration using glob expansion is one of the most common. You can find details in the documentation.
It's pretty obvious:
In every directory (**), take everything (/*) from assets/script/ that ends with .coffee. Put it into assets/script. Rename extensions to .js.
So, your task can probably be configured like that:
md2pdf: {
main: {
files: [ {
expand: true,
src: ['**/*.md', '!node_modules/**/*'],
dest: "pdf/"
} ]
}
}
Ok figured it out !
module.exports = function(grunt) {
// 2 - Plugins
grunt.loadNpmTasks('grunt-md2pdf');
grunt.registerTask('default', 'Dynamically generate PDF from MD', function() {
grunt.file.expand("./**/*.md").forEach( function(file) {
if(!file.match('./node_modules')) {
var md2pdf = grunt.config.get('md2pdf') || {};
md2pdf[file] = {
src: file,
dest: file + '.pdf'
};
grunt.config.set('md2pdf', md2pdf);
}
});
grunt.task.run('md2pdf');
});
};

Interpolated array in Grunt template is interpreted as a string

Previous title: "Why is Grunt's concat task not using dynamic configuration values?"
I am trying to dynamically configure the files that are concatenated by Grunt, and in doing so I came across this issue where the grunt-contrib-concat plugin does not seem to pick up the dynamically set values. At first I thought I was doing something wrong, but after creating my own task and using the same dynamic values everything came out just as intended. So that leaves the question of why is the grunt concat task not doing picking up and using the same values?
A gruntfile that reproduces the behaviour is seen below (gist: fatso83/73875acd1fa3662ef360).
// Grunt file that shows how dynamic config (and option!) values
// are not used in the grunt-contrib-concat task. Run using 'grunt'
module.exports = function(grunt){
grunt.initConfig({
concat : {
foo : {
nonull : true,
src: '<%= grunt.config.get("myfiles") %>',
dest : 'outfile.txt'
}
},
myTask : {
bar : '<%= grunt.config.get("myfiles") %>'
}
});
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.registerMultiTask('myTask', function() {
grunt.log.writeln('myTask:' + this.target + ' data=' + this.data);
});
grunt.registerTask('default', ['myTask','concat']);
grunt.config.set('myfiles',['file1.txt', 'file2.txt'])
}
EDIT: A new lead:
After literally hours of going nowhere I came across this sentence on Grunt's homepage:
nonull If set to true then the operation will include non-matching
patterns. Combined with grunt's --verbose flag, this option can help
debug file path issues.
Adding that to the config (edited above to reflect this) I got this error message which at least show that something is doing something with the dynamic values:
Running "concat:foo" (concat) task
>> Source file "file1.txt,file2.txt" not found.
Warning: Unable to write "outfile.txt/file1.txt,file2.txt" file
(Error code: ENOTDIR). Use --force to continue.
After some more debugging in the other task, myTask, I have found out that the data sent in to the task as this.data is a string value, not an array. This is perhaps not very surprising, given that we do string interpolation, but this is not consistent with other interpolation features. For instance will <%= otherTask.fooTarget.src %> get the other task's src property as an array value.
Now the question is really how can I avoid passing the interpolated value as an array, rather than a string, to the concat task?
Updated after reading up on the Grunt source code
After I found out that the problem was our array was interpreted as a string I quickly found a related question with a solution that seemed promising. Simply by enclosing the interpolated array string with curly brackets Grunt was able to find the files!
Unfortunately, the globbing pattern we are effectively creating does not preserve the specified file order. In the related question above I posted a thorough explanation of what was going on and how you can work around it in the general case.
For my specific case, where I reference a field in the configuration object there is actually no need for a function call to retreive it as it is directly available in the templates own scope! Therefore, instead of calling grunt.config.get('myfiles'), I can simply do <%= myfiles %>.
For the example above:
grunt.initConfig({
concat : {
foo : {
nonull : true,
src: '<%= myfiles %>',
dest : 'outfile.txt'
}
},
myTask : {
bar : '<%= myfiles %>'
}
});

Resources