Skip to content

Commit bd693dd

Browse files
authored
issue/1802: inlined fix for duplicate handlebars namespaces (#1803)
1 parent ccf3ca1 commit bd693dd

2 files changed

Lines changed: 241 additions & 1 deletion

File tree

grunt/tasks/handlebars.js

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
/*
2+
* grunt-contrib-handlebars
3+
* http://gruntjs.com/
4+
*
5+
* Copyright (c) 2016 Tim Branyen, contributors
6+
* Licensed under the MIT license.
7+
*/
8+
9+
'use strict';
10+
var chalk = require('chalk');
11+
var nsdeclare = require('nsdeclare');
12+
13+
module.exports = function(grunt) {
14+
var _ = grunt.util._;
15+
16+
// content conversion for templates
17+
var defaultProcessContent = function(content) { return content; };
18+
19+
// AST processing for templates
20+
var defaultProcessAST = function(ast) { return ast; };
21+
22+
// filename conversion for templates
23+
var defaultProcessName = function(name) { return name; };
24+
25+
// filename conversion for partials
26+
var defaultProcessPartialName = function(filepath) {
27+
var pieces = _.last(filepath.split('/')).split('.');
28+
var name = _(pieces).without(_.last(pieces)).join('.'); // strips file extension
29+
if (name.charAt(0) === '_') {
30+
name = name.substr(1, name.length); // strips leading _ character
31+
}
32+
return name;
33+
};
34+
35+
var extractGlobalNamespace = function(nsDeclarations) {
36+
// Extract global namespace from any existing namespace declaration.
37+
// The purpose of this method is too fix an issue with AMD when using namespace as a function where the
38+
// nsInfo.namespace will contains the last namespace, not the global namespace.
39+
40+
var declarations = _.keys(nsDeclarations);
41+
42+
// no declaration found
43+
if (!declarations.length) {
44+
return '';
45+
}
46+
47+
// In case only one namespace has been declared it will only return it.
48+
if (declarations.length === 1) {
49+
return declarations[0];
50+
}
51+
// We only need to take any declaration to extract the global namespace.
52+
// Another option might be find the shortest declaration which is the global one.
53+
var matches = declarations[0].match(/(this\[[^\[]+\])/g);
54+
return matches[0];
55+
};
56+
57+
grunt.registerMultiTask('handlebars', 'Compile handlebars templates and partials.', function() {
58+
var options = this.options({
59+
namespace: 'JST',
60+
separator: grunt.util.linefeed + grunt.util.linefeed,
61+
wrapped: true,
62+
amd: false,
63+
commonjs: false,
64+
knownHelpers: [],
65+
knownHelpersOnly: false
66+
});
67+
68+
// assign regex for partials directory detection
69+
var partialsPathRegex = options.partialsPathRegex || /./;
70+
71+
// assign regex for partial detection
72+
var isPartialRegex = options.partialRegex || /^_/;
73+
74+
// assign transformation functions
75+
var processContent = options.processContent || defaultProcessContent;
76+
var processName = options.processName || defaultProcessName;
77+
var processPartialName = options.processPartialName || defaultProcessPartialName;
78+
var processAST = options.processAST || defaultProcessAST;
79+
var useNamespace = options.namespace !== false;
80+
81+
// assign compiler options
82+
var compilerOptions = options.compilerOptions || {};
83+
var filesCount = 0;
84+
85+
this.files.forEach(function(f) {
86+
var declarations = [];
87+
var partials = {};
88+
var templates = {};
89+
// template identifying parts
90+
var ast, compiled, filename;
91+
92+
// Namespace info for current template
93+
var nsInfo;
94+
95+
// Map of already declared namespace parts
96+
var nsDeclarations = {};
97+
98+
// nsdeclare options when fetching namespace info
99+
var nsDeclareOptions = {response: 'details', declared: nsDeclarations};
100+
101+
// Just get the namespace info for a given template
102+
var getNamespaceInfo = _.memoize(function(filepath) {
103+
if (!useNamespace) {
104+
return undefined;
105+
}
106+
if (_.isFunction(options.namespace)) {
107+
return nsdeclare(options.namespace(filepath), nsDeclareOptions);
108+
}
109+
return nsdeclare(options.namespace, nsDeclareOptions);
110+
});
111+
112+
// iterate files, processing partials and templates separately
113+
f.src.filter(function(filepath) {
114+
// Warn on and remove invalid source files (if nonull was set).
115+
if (!grunt.file.exists(filepath)) {
116+
grunt.log.warn('Source file "' + filepath + '" not found.');
117+
return false;
118+
}
119+
return true;
120+
})
121+
.forEach(function(filepath) {
122+
var src = processContent(grunt.file.read(filepath), filepath);
123+
124+
var Handlebars = require('handlebars');
125+
try {
126+
// parse the handlebars template into it's AST
127+
ast = processAST(Handlebars.parse(src));
128+
compiled = Handlebars.precompile(ast, compilerOptions);
129+
130+
// if configured to, wrap template in Handlebars.template call
131+
if (options.wrapped === true) {
132+
compiled = 'Handlebars.template(' + compiled + ')';
133+
}
134+
} catch (e) {
135+
grunt.log.error(e);
136+
grunt.fail.warn('Handlebars failed to compile ' + filepath + '.');
137+
}
138+
139+
// register partial or add template to namespace
140+
if (partialsPathRegex.test(filepath) && isPartialRegex.test(_.last(filepath.split('/')))) {
141+
filename = processPartialName(filepath);
142+
if (options.partialsUseNamespace === true) {
143+
nsInfo = getNamespaceInfo(filepath);
144+
if (nsInfo.declaration) {
145+
declarations.push(nsInfo.declaration);
146+
}
147+
partials[nsInfo.namespace + ':' + JSON.stringify(filename)] = ('Handlebars.registerPartial(' +
148+
JSON.stringify(filename) + ', ' + nsInfo.namespace + '[' + JSON.stringify(filename) + '] = ' +
149+
compiled + ');');
150+
} else {
151+
partials[JSON.stringify(filename)] = ('Handlebars.registerPartial(' + JSON.stringify(filename) +
152+
', ' + compiled + ');');
153+
}
154+
} else {
155+
if ((options.amd || options.commonjs) && !useNamespace) {
156+
compiled = 'return ' + compiled;
157+
}
158+
filename = processName(filepath);
159+
if (useNamespace) {
160+
nsInfo = getNamespaceInfo(filepath);
161+
if (nsInfo.declaration) {
162+
declarations.push(nsInfo.declaration);
163+
}
164+
templates[nsInfo.namespace + ':' + JSON.stringify(filename)] = (nsInfo.namespace + '[' +
165+
JSON.stringify(filename) + '] = ' + compiled + ';');
166+
} else if (options.commonjs === true) {
167+
templates[JSON.stringify(filename)] = compiled + ';';
168+
} else {
169+
templates[JSON.stringify(filename)] = compiled;
170+
}
171+
}
172+
});
173+
174+
var output = declarations.concat(_.values(partials), _.values(templates));
175+
if (output.length < 1) {
176+
grunt.log.warn('Destination not written because compiled files were empty.');
177+
} else {
178+
if (useNamespace) {
179+
if (options.node) {
180+
output.unshift('Handlebars = glob.Handlebars || require(\'handlebars\');');
181+
output.unshift('var glob = (\'undefined\' === typeof window) ? global : window,');
182+
183+
var nodeExport = 'if (typeof exports === \'object\' && exports) {';
184+
nodeExport += 'module.exports = ' + nsInfo.namespace + ';}';
185+
186+
output.push(nodeExport);
187+
}
188+
189+
}
190+
191+
if (options.amd) {
192+
// Wrap the file in an AMD define fn.
193+
if (typeof options.amd === 'boolean') {
194+
output.unshift('define([\'handlebars\'], function(Handlebars) {');
195+
} else if (typeof options.amd === 'string') {
196+
output.unshift('define([\'' + options.amd + '\'], function(Handlebars) {');
197+
} else if (typeof options.amd === 'function') {
198+
output.unshift('define([\'' + options.amd(filename, ast, compiled) + '\'], function(Handlebars) {');
199+
} else if (Array.isArray(options.amd)) {
200+
// convert options.amd to a string of dependencies for require([...])
201+
var amdString = '';
202+
for (var i = 0; i < options.amd.length; i++) {
203+
if (i !== 0) {
204+
amdString += ', ';
205+
}
206+
207+
amdString += '\'' + options.amd[i] + '\'';
208+
}
209+
210+
// Wrap the file in an AMD define fn.
211+
output.unshift('define([' + amdString + '], function(Handlebars) {');
212+
}
213+
214+
if (useNamespace) {
215+
// Namespace has not been explicitly set to false; the AMD
216+
// wrapper will return the object containing the template.
217+
output.push('return ' + extractGlobalNamespace(nsDeclarations) + ';');
218+
}
219+
output.push('});');
220+
}
221+
222+
if (options.commonjs) {
223+
if (useNamespace) {
224+
output.push('return ' + nsInfo.namespace + ';');
225+
}
226+
// Export the templates object for CommonJS environments.
227+
output.unshift('module.exports = function(Handlebars) {');
228+
output.push('};');
229+
}
230+
231+
filesCount++;
232+
grunt.file.write(f.dest, output.join(grunt.util.normalizelf(options.separator)));
233+
grunt.verbose.writeln('File ' + chalk.cyan(f.dest) + ' created.');
234+
}
235+
});
236+
237+
grunt.log.ok(filesCount + ' ' + grunt.util.pluralize(filesCount, 'file/files') + ' created.');
238+
});
239+
};

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@
138138
"grunt-contrib-clean": "~1.0.0",
139139
"grunt-contrib-connect": "~1.0.2",
140140
"grunt-contrib-copy": "~1.0.0",
141-
"grunt-contrib-handlebars": "~1.0.0",
141+
"handlebars": "~4.0.0",
142+
"nsdeclare": "0.1.0",
142143
"grunt-contrib-jshint": "~1.0.0",
143144
"grunt-contrib-uglify": "^3.0.1",
144145
"grunt-contrib-watch": "~1.0.0",

0 commit comments

Comments
 (0)