1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | var fs = require('fs');
|
11 | var path = require('path');
|
12 |
|
13 | var semver = require('semver');
|
14 |
|
15 | var prompt = require('prompt');
|
16 | prompt.message = '[' + '?'.green + ']';
|
17 | prompt.delimiter = ' ';
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 | var defaults;
|
25 | function getDefaults() {
|
26 | if (defaults) { return defaults; }
|
27 | defaults = {};
|
28 |
|
29 | var filepaths = file.taskfiles('init/defaults.json');
|
30 |
|
31 | if (filepaths.length) {
|
32 | verbose.subhead('Loading defaults');
|
33 |
|
34 |
|
35 | filepaths.forEach(function(filepath) {
|
36 | underscore.defaults(defaults, file.readJson(filepath));
|
37 | });
|
38 | }
|
39 | }
|
40 |
|
41 |
|
42 | function availableLicenses() {
|
43 | return file.taskpaths('init/licenses').reduce(function(arr, filepath) {
|
44 | return arr.concat(fs.readdirSync(filepath).map(function(filename) {
|
45 | return filename.replace(/^LICENSE-/, '');
|
46 | }));
|
47 | }, []);
|
48 | }
|
49 |
|
50 | task.registerInitTask('init', 'Initialize a project from a predefined template.', function() {
|
51 |
|
52 | var args = util.toArray(arguments);
|
53 |
|
54 | var name = args.shift();
|
55 |
|
56 | var templates = {};
|
57 |
|
58 | var searchpaths = [];
|
59 |
|
60 |
|
61 | this.extraspaths().reverse().forEach(function(dirpath) {
|
62 | var obj = {path: dirpath, subdirs: []};
|
63 | searchpaths.unshift(obj);
|
64 |
|
65 | fs.readdirSync(dirpath).forEach(function(filename) {
|
66 | var filepath = path.join(dirpath, filename);
|
67 | if (fs.statSync(filepath).isDirectory()) {
|
68 |
|
69 | obj.subdirs.push(filename);
|
70 | } else if (fs.statSync(filepath).isFile() && path.extname(filepath) === '.js') {
|
71 |
|
72 | templates[path.basename(filename, '.js')] = path.join(dirpath, filename);
|
73 | }
|
74 | });
|
75 | });
|
76 |
|
77 |
|
78 | if (!(name && name in templates)) {
|
79 | log.error('A valid template name must be specified. Valid templates are: ' +
|
80 | log.wordlist(Object.keys(templates)) + '.');
|
81 | return false;
|
82 | }
|
83 |
|
84 |
|
85 | if (path.existsSync(path.join(process.cwd(), 'grunt.js'))) {
|
86 | fail.warn('Beware, grunt.js file already exists.');
|
87 | }
|
88 |
|
89 |
|
90 | var taskDone = this.async();
|
91 |
|
92 |
|
93 | var init = {
|
94 |
|
95 | defaults: getDefaults(),
|
96 |
|
97 | srcpath: file.taskfile.bind(file, 'init', name),
|
98 |
|
99 | destpath: path.join.bind(path, process.cwd()),
|
100 |
|
101 |
|
102 | addLicenseFiles: function(files, licenses) {
|
103 | var available = availableLicenses();
|
104 | licenses.forEach(function(license) {
|
105 | files.push({
|
106 | src: '../licenses/LICENSE-' + license,
|
107 | dest: 'LICENSE-' + license
|
108 | });
|
109 | });
|
110 | },
|
111 |
|
112 |
|
113 | copy: function(srcpath, destpath, callback) {
|
114 | if (typeof destpath !== 'string') {
|
115 | callback = destpath;
|
116 | destpath = srcpath;
|
117 | }
|
118 | var abssrcpath = init.srcpath(srcpath);
|
119 | var absdestpath = init.destpath(destpath);
|
120 | if (!path.existsSync(abssrcpath)) {
|
121 | abssrcpath = init.srcpath('../misc/placeholder');
|
122 | }
|
123 | verbose.or.write('Writing ' + destpath + '...');
|
124 | try {
|
125 | file.copy(abssrcpath, absdestpath, callback);
|
126 | verbose.or.ok();
|
127 | } catch(e) {
|
128 | verbose.or.error();
|
129 | throw e;
|
130 | }
|
131 | },
|
132 |
|
133 |
|
134 | copyAndProcess: function(files, props) {
|
135 | files.forEach(function(files) {
|
136 | init.copy(files.src, files.dest || files.src, function(contents) {
|
137 | return template.process(contents, props, 'init');
|
138 | });
|
139 | });
|
140 | },
|
141 |
|
142 |
|
143 | writePackage: function(filename, props, callback) {
|
144 | var pkg = {};
|
145 |
|
146 | ['name', 'title', 'description', 'version', 'homepage'].forEach(function(prop) {
|
147 | if (prop in props) { pkg[prop] = props[prop]; }
|
148 | });
|
149 |
|
150 | ['name', 'email', 'url'].forEach(function(prop) {
|
151 | if (props['author_' + prop]) {
|
152 | if (!pkg.author) { pkg.author = {}; }
|
153 | pkg.author[prop] = props['author_' + prop];
|
154 | }
|
155 | });
|
156 |
|
157 | if ('repository' in props) { pkg.repository = {type: 'git', url: props.repository}; }
|
158 | if ('bugs' in props) { pkg.bugs = {url: props.bugs}; }
|
159 | pkg.licenses = props.licenses.map(function(license) {
|
160 | return {type: license, url: props.homepage + '/blob/master/LICENSE-' + license};
|
161 | });
|
162 | pkg.dependencies = {};
|
163 | pkg.devDependencies = {};
|
164 | pkg.keywords = [];
|
165 |
|
166 | if (props.node_version) { pkg.engines = {node: props.node_version}; }
|
167 | if (props.node_main) { pkg.main = props.node_main; }
|
168 | if (props.node_test) {
|
169 | pkg.scripts = {test: props.node_test};
|
170 | if (props.node_test.split(' ')[0] === 'grunt') {
|
171 | pkg.devDependencies.grunt = '~' + grunt.version;
|
172 | }
|
173 | }
|
174 |
|
175 |
|
176 | if (callback) { pkg = callback(pkg, props); }
|
177 |
|
178 |
|
179 | file.write(init.destpath(filename), JSON.stringify(pkg, null, 2));
|
180 | }
|
181 | };
|
182 |
|
183 |
|
184 | init.flags = {};
|
185 | args.forEach(function(flag) { init.flags[flag] = true; });
|
186 |
|
187 |
|
188 |
|
189 | require(templates[name]).apply(null, [init, function() {
|
190 |
|
191 | if (task.hadErrors()) { taskDone(false); }
|
192 |
|
193 | log.writeln().writeln('Initialized from template "' + name + '".');
|
194 |
|
195 | taskDone();
|
196 | }].concat(args));
|
197 | });
|
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 | task.registerHelper('prompt', function(defaults, options, done) {
|
205 |
|
206 | if (util.kindOf(defaults) === 'array') {
|
207 | done = options;
|
208 | options = defaults;
|
209 | defaults = {};
|
210 | }
|
211 |
|
212 |
|
213 | var sanitize = {};
|
214 | options.forEach(function(option) {
|
215 | if (option.sanitize) {
|
216 | sanitize[option.name] = option.sanitize;
|
217 | }
|
218 | });
|
219 |
|
220 |
|
221 | options.push({
|
222 | message: 'Are these answers correct?'.green,
|
223 | name: 'ANSWERS_VALID',
|
224 | default: 'Y/n'
|
225 | });
|
226 |
|
227 |
|
228 |
|
229 | (function ask() {
|
230 | log.subhead('Please answer the following:');
|
231 | var result = underscore.clone(defaults);
|
232 |
|
233 | async.forEachSeries(options, function(option, done) {
|
234 |
|
235 | function doPrompt() {
|
236 | prompt.start();
|
237 | prompt.getInput(option, function(err, line) {
|
238 | if (err) { return done(err); }
|
239 | result[option.name] = line;
|
240 | done();
|
241 | });
|
242 | }
|
243 |
|
244 |
|
245 | if (typeof option.default === 'function') {
|
246 | option.default(result, function(err, value) {
|
247 |
|
248 | option.default = err ? '???' : value;
|
249 | doPrompt();
|
250 | });
|
251 | } else {
|
252 | doPrompt();
|
253 | }
|
254 | }, function(err) {
|
255 |
|
256 | if (/y/i.test(result.ANSWERS_VALID)) {
|
257 |
|
258 | prompt.pause();
|
259 |
|
260 | delete result.ANSWERS_VALID;
|
261 |
|
262 | Object.keys(result).forEach(function(name) {
|
263 |
|
264 | if (sanitize[name]) {
|
265 | result[name] = sanitize[name](result[name], result);
|
266 | }
|
267 |
|
268 | if (result[name] === 'none') {
|
269 | result[name] = '';
|
270 | }
|
271 | });
|
272 |
|
273 | log.writeln();
|
274 | done(err, result);
|
275 | } else {
|
276 |
|
277 | options.slice(0, -1).forEach(function(option) {
|
278 | option.default = result[option.name];
|
279 | });
|
280 |
|
281 | ask();
|
282 | }
|
283 | });
|
284 | }());
|
285 | });
|
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 | var prompts = {
|
292 | name: {
|
293 | message: 'Project name',
|
294 | default: function(data, done) {
|
295 | var type = data.type || '';
|
296 |
|
297 |
|
298 |
|
299 | var re = new RegExp('^' + type + '[\\-\\._]?|(?:[\\-\\._]?' + type + ')?(?:[\\-\\._]?js)?$', 'ig');
|
300 |
|
301 | var name = path.basename(process.cwd()).replace(re, '');
|
302 |
|
303 | name = name.replace(/[^\w\-\.]/g, '');
|
304 | done(null, name);
|
305 | },
|
306 | validator: /^[\w\-\.]+$/,
|
307 | warning: 'Name must be only letters, numbers, dashes, dots or underscores.',
|
308 | sanitize: function(value, obj) {
|
309 |
|
310 | obj.js_safe_name = value.replace(/[\W_]+/g, '_').replace(/^(\d)/, '_$1');
|
311 |
|
312 | return value;
|
313 | }
|
314 | },
|
315 | title: {
|
316 | message: 'Project title',
|
317 | default: function(data, done) {
|
318 | var title = data.name || '';
|
319 | title = title.replace(/[\W_]+/g, ' ');
|
320 | title = title.replace(/\w+/g, function(word) {
|
321 | return word[0].toUpperCase() + word.slice(1).toLowerCase();
|
322 | });
|
323 | done(null, title);
|
324 | }
|
325 | },
|
326 | description: {
|
327 | message: 'Description',
|
328 | default: 'The best project ever.'
|
329 | },
|
330 | version: {
|
331 | message: 'Version',
|
332 | default: function(data, done) {
|
333 |
|
334 | task.helper('child_process', {
|
335 | cmd: 'git',
|
336 | args: ['describe', '--tags']
|
337 | }, function(err, result) {
|
338 | if (result) {
|
339 | result = result.split('-')[0];
|
340 | }
|
341 | done(null, semver.valid(result) || '0.1.0');
|
342 | });
|
343 | },
|
344 | validator: semver.valid,
|
345 | warning: 'Must be a valid semantic version.'
|
346 | },
|
347 | repository: {
|
348 | message: 'Project git repository',
|
349 | default: function(data, done) {
|
350 |
|
351 | task.helper('git_origin', function(err, result) {
|
352 | if (!err) {
|
353 | result = result.replace(/^git@([^:]+):/, 'git://$1/');
|
354 | }
|
355 | done(null, result);
|
356 | });
|
357 | }
|
358 | },
|
359 | homepage: {
|
360 | message: 'Project homepage',
|
361 |
|
362 | default: function(data, done) {
|
363 | done(null, task.helper('github_web_url', data.repository) || 'none');
|
364 | }
|
365 | },
|
366 | bugs: {
|
367 | message: 'Project issues tracker',
|
368 |
|
369 | default: function(data, done) {
|
370 | done(null, task.helper('github_web_url', data.repository, 'issues') || 'none');
|
371 | }
|
372 | },
|
373 | licenses: {
|
374 | message: 'Licenses',
|
375 | default: 'MIT',
|
376 | validator: /^[\w\-]+(?:\s+[\w\-]+)*$/,
|
377 | warning: 'Must be one or more space-separated licenses. (eg. ' +
|
378 | availableLicenses().join(' ') + ')',
|
379 |
|
380 | sanitize: function(value) { return value.split(/\s+/); }
|
381 | },
|
382 | author_name: {
|
383 | message: 'Author name',
|
384 | default: function(data, done) {
|
385 |
|
386 | task.helper('child_process', {
|
387 | cmd: 'git',
|
388 | args: ['config', '--get', 'user.name'],
|
389 | fallback: 'none'
|
390 | }, done);
|
391 | }
|
392 | },
|
393 | author_email: {
|
394 | message: 'Author email',
|
395 | default: function(data, done) {
|
396 |
|
397 | task.helper('child_process', {
|
398 | cmd: 'git',
|
399 | args: ['config', '--get', 'user.email'],
|
400 | fallback: 'none'
|
401 | }, done);
|
402 | }
|
403 | },
|
404 | author_url: {
|
405 | message: 'Author url',
|
406 | default: 'none'
|
407 | },
|
408 | node_version: {
|
409 | message: 'What versions of node does it run on?',
|
410 | default: '>= ' + process.versions.node
|
411 | },
|
412 | node_main: {
|
413 | message: 'Main module/entry point',
|
414 | default: function(data, done) {
|
415 | done(null, 'lib/' + data.name);
|
416 | }
|
417 | },
|
418 | node_test: {
|
419 | message: 'Test command',
|
420 | default: 'grunt test'
|
421 | }
|
422 | };
|
423 |
|
424 |
|
425 | task.registerHelper('prompt_for_obj', function() {
|
426 | return prompts;
|
427 | });
|
428 |
|
429 |
|
430 | task.registerHelper('prompt_for', function(name, alternateDefault) {
|
431 |
|
432 | var option = underscore.clone(prompts[name]);
|
433 | option.name = name;
|
434 |
|
435 | if (name in getDefaults()) {
|
436 |
|
437 | option.default = getDefaults()[name];
|
438 | } else if (arguments.length === 2) {
|
439 |
|
440 | option.default = alternateDefault;
|
441 | }
|
442 | return option;
|
443 | });
|
444 |
|
445 |
|
446 | task.registerHelper('git_origin', function(done) {
|
447 | task.helper('child_process', {
|
448 | cmd: 'git',
|
449 | args: ['remote', '-v']
|
450 | }, function(err, result) {
|
451 | var re = /^origin/;
|
452 | if (err || !result) {
|
453 | done(true, 'none');
|
454 | } else {
|
455 | result = result.split('\n').filter(re.test.bind(re))[0];
|
456 | done(null, result.split(/\s/)[1]);
|
457 | }
|
458 | });
|
459 | });
|
460 |
|
461 |
|
462 | var githubWebUrlRe = /^.+(?:@|:\/\/)(github.com)[:\/](.+?)(?:\.git|\/)?$/;
|
463 | task.registerHelper('github_web_url', function(uri, suffix) {
|
464 | var matches = githubWebUrlRe.exec(uri);
|
465 | if (!matches) { return null; }
|
466 | var url = 'https://' + matches[1] + '/' + matches[2];
|
467 | if (suffix) {
|
468 | url += '/' + suffix.replace(/^\//, '');
|
469 | }
|
470 | return url;
|
471 | });
|