1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | module.exports = function(grunt) {
|
11 |
|
12 |
|
13 | var fs = require('fs');
|
14 | var path = require('path');
|
15 |
|
16 |
|
17 | var semver = require('semver');
|
18 |
|
19 | var prompt = require('prompt');
|
20 | prompt.message = '[' + '?'.green + ']';
|
21 | prompt.delimiter = ' ';
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 | function availableLicenses() {
|
29 | return grunt.task.expandFiles('init/licenses/*').map(function(obj) {
|
30 | return path.basename(String(obj)).replace(/^LICENSE-/, '');
|
31 | });
|
32 | }
|
33 |
|
34 | grunt.registerInitTask('init', 'Generate project scaffolding from a predefined template.', function() {
|
35 |
|
36 | var args = grunt.utils.toArray(arguments);
|
37 |
|
38 | var name = args.shift();
|
39 |
|
40 |
|
41 |
|
42 | if (name == null) {
|
43 | name = grunt._npmTasks[grunt._npmTasks.length - 1];
|
44 | }
|
45 |
|
46 | var templates = {};
|
47 | grunt.task.expandFiles('init/*.js').forEach(function(fileobj) {
|
48 |
|
49 | templates[path.basename(fileobj.abs, '.js')] = require(fileobj.abs);
|
50 | });
|
51 | var initTemplate = templates[name];
|
52 |
|
53 |
|
54 | grunt.log.writelns(
|
55 | 'This task will create one or more files in the current directory, ' +
|
56 | 'based on the environment and the answers to a few questions. ' +
|
57 | 'Note that answering "?" to any question will show question-specific ' +
|
58 | 'help and answering "none" to most questions will leave its value blank.'
|
59 | );
|
60 |
|
61 |
|
62 | if (!initTemplate) {
|
63 | grunt.log.writeln().write('Loading' + (name ? ' "' + name + '"' : '') + ' init template...').error();
|
64 | grunt.log.errorlns('A valid template name must be specified, eg. "grunt ' +
|
65 | 'init:commonjs". The currently-available init templates are: ');
|
66 | Object.keys(templates).forEach(function(name) {
|
67 | var description = templates[name].description || '(no description)';
|
68 | grunt.log.errorlns(name.cyan + ' - ' + description);
|
69 | });
|
70 | return false;
|
71 | }
|
72 |
|
73 |
|
74 |
|
75 | if (initTemplate.warnOn && grunt.file.expand(initTemplate.warnOn).length > 0) {
|
76 | grunt.log.writeln();
|
77 | grunt.warn('Existing files may be overwritten!');
|
78 | }
|
79 |
|
80 |
|
81 | var taskDone = this.async();
|
82 |
|
83 | var pathPrefix = 'init/' + name + '/root/';
|
84 |
|
85 |
|
86 | var init = {
|
87 |
|
88 | defaults: grunt.task.readDefaults('init/defaults.json'),
|
89 |
|
90 | renames: grunt.task.readDefaults('init', name, 'rename.json'),
|
91 |
|
92 |
|
93 |
|
94 | filesToCopy: function(props) {
|
95 | var files = {};
|
96 |
|
97 | grunt.task.expandFiles({dot: true}, pathPrefix + '**').forEach(function(obj) {
|
98 |
|
99 | var relpath = obj.rel.slice(pathPrefix.length);
|
100 | var rule = init.renames[relpath];
|
101 |
|
102 | if (!rule && relpath in init.renames) { return; }
|
103 |
|
104 | files[rule ? grunt.template.process(rule, props, 'init') : relpath] = obj.rel;
|
105 | });
|
106 | return files;
|
107 | },
|
108 |
|
109 | srcpath: function(arg1) {
|
110 | if (arg1 == null) { return null; }
|
111 | var args = ['init', name, 'root'].concat(grunt.utils.toArray(arguments));
|
112 | return grunt.task.getFile.apply(grunt.file, args);
|
113 | },
|
114 |
|
115 | destpath: path.join.bind(path, process.cwd()),
|
116 |
|
117 |
|
118 | addLicenseFiles: function(files, licenses) {
|
119 | var available = availableLicenses();
|
120 | licenses.forEach(function(license) {
|
121 | var fileobj = grunt.task.expandFiles('init/licenses/LICENSE-' + license)[0];
|
122 | files['LICENSE-' + license] = fileobj ? fileobj.rel : null;
|
123 | });
|
124 | },
|
125 |
|
126 |
|
127 |
|
128 | copy: function(srcpath, destpath, options) {
|
129 |
|
130 | if (typeof destpath !== 'string') {
|
131 | options = destpath;
|
132 | destpath = srcpath;
|
133 | }
|
134 |
|
135 | if (!grunt.file.isPathAbsolute(srcpath)) {
|
136 | srcpath = init.srcpath(srcpath);
|
137 | }
|
138 |
|
139 | if (!srcpath) {
|
140 | srcpath = grunt.task.getFile('init/misc/placeholder');
|
141 | }
|
142 | grunt.verbose.or.write('Writing ' + destpath + '...');
|
143 | try {
|
144 | grunt.file.copy(srcpath, init.destpath(destpath), options);
|
145 | grunt.verbose.or.ok();
|
146 | } catch(e) {
|
147 | grunt.verbose.or.error().error(e);
|
148 | throw e;
|
149 | }
|
150 | },
|
151 |
|
152 |
|
153 | copyAndProcess: function(files, props, options) {
|
154 | options = grunt.utils._.defaults(options || {}, {
|
155 | process: function(contents) {
|
156 | return grunt.template.process(contents, props, 'init');
|
157 | }
|
158 | });
|
159 | Object.keys(files).forEach(function(destpath) {
|
160 | var o = Object.create(options);
|
161 | var srcpath = files[destpath];
|
162 |
|
163 |
|
164 | var relpath;
|
165 | if (srcpath && !grunt.file.isPathAbsolute(srcpath)) {
|
166 | if (o.noProcess) {
|
167 | relpath = srcpath.slice(pathPrefix.length);
|
168 | o.noProcess = grunt.file.isMatch(o.noProcess, relpath);
|
169 | }
|
170 | srcpath = grunt.task.getFile(srcpath);
|
171 | }
|
172 |
|
173 | init.copy(srcpath, destpath, o);
|
174 | });
|
175 | },
|
176 |
|
177 |
|
178 | writePackageJSON: function(filename, props, callback) {
|
179 | var pkg = {};
|
180 |
|
181 | ['name', 'title', 'description', 'version', 'homepage'].forEach(function(prop) {
|
182 | if (prop in props) { pkg[prop] = props[prop]; }
|
183 | });
|
184 |
|
185 | var hasAuthor = Object.keys(props).some(function(prop) {
|
186 | return (/^author_/).test(prop);
|
187 | });
|
188 | if (hasAuthor) {
|
189 | pkg.author = {};
|
190 | ['name', 'email', 'url'].forEach(function(prop) {
|
191 | if (props['author_' + prop]) {
|
192 | pkg.author[prop] = props['author_' + prop];
|
193 | }
|
194 | });
|
195 | }
|
196 |
|
197 | if ('repository' in props) { pkg.repository = {type: 'git', url: props.repository}; }
|
198 | if ('bugs' in props) { pkg.bugs = {url: props.bugs}; }
|
199 | if (props.licenses) {
|
200 | pkg.licenses = props.licenses.map(function(license) {
|
201 | return {type: license, url: props.homepage + '/blob/master/LICENSE-' + license};
|
202 | });
|
203 | }
|
204 |
|
205 |
|
206 | if (props.main) { pkg.main = props.main; }
|
207 | if (props.bin) { pkg.bin = props.bin; }
|
208 | if (props.node_version) { pkg.engines = {node: props.node_version}; }
|
209 | if (props.npm_test) {
|
210 | pkg.scripts = {test: props.npm_test};
|
211 | if (props.npm_test.split(' ')[0] === 'grunt') {
|
212 | if (!props.devDependencies) { props.devDependencies = {}; }
|
213 | props.devDependencies.grunt = '~' + grunt.version;
|
214 | }
|
215 | }
|
216 |
|
217 | if (props.dependencies) { pkg.dependencies = props.dependencies; }
|
218 | if (props.devDependencies) { pkg.devDependencies = props.devDependencies; }
|
219 | if (props.keywords) { pkg.keywords = props.keywords; }
|
220 |
|
221 |
|
222 | if (callback) { pkg = callback(pkg, props); }
|
223 |
|
224 |
|
225 | grunt.file.write(init.destpath(filename), JSON.stringify(pkg, null, 2));
|
226 | }
|
227 | };
|
228 |
|
229 |
|
230 | init.flags = {};
|
231 | args.forEach(function(flag) { init.flags[flag] = true; });
|
232 |
|
233 |
|
234 | if (initTemplate.notes) {
|
235 | grunt.log.subhead('"' + name + '" template notes:').writelns(initTemplate.notes);
|
236 | }
|
237 |
|
238 |
|
239 |
|
240 | initTemplate.template.apply(this, [grunt, init, function() {
|
241 |
|
242 | if (grunt.task.current.errorCount) { taskDone(false); }
|
243 |
|
244 | grunt.log.writeln().writeln('Initialized from template "' + name + '".');
|
245 |
|
246 | taskDone();
|
247 | }].concat(args));
|
248 | });
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 | grunt.registerHelper('prompt', function(defaults, options, done) {
|
256 |
|
257 | if (grunt.utils.kindOf(defaults) === 'array') {
|
258 | done = options;
|
259 | options = defaults;
|
260 | defaults = {};
|
261 | }
|
262 |
|
263 |
|
264 | var sanitize = {};
|
265 | options.forEach(function(option) {
|
266 | if (option.sanitize) {
|
267 | sanitize[option.name] = option.sanitize;
|
268 | }
|
269 | });
|
270 |
|
271 |
|
272 | if (options.length > 0) {
|
273 | options.push({
|
274 | message: 'Do you need to make any changes to the above before continuing?'.green,
|
275 | name: 'ANSWERS_VALID',
|
276 | default: 'y/N'
|
277 | });
|
278 | }
|
279 |
|
280 |
|
281 |
|
282 | (function ask() {
|
283 | grunt.log.subhead('Please answer the following:');
|
284 | var result = grunt.utils._.clone(defaults);
|
285 |
|
286 | grunt.utils.async.forEachSeries(options, function(option, done) {
|
287 | var defaultValue;
|
288 | grunt.utils.async.forEachSeries(['default', 'altDefault'], function(prop, next) {
|
289 | if (typeof option[prop] === 'function') {
|
290 |
|
291 |
|
292 | option[prop](defaultValue, result, function(err, value) {
|
293 | defaultValue = String(value);
|
294 | next();
|
295 | });
|
296 | } else {
|
297 |
|
298 | if (prop in option) {
|
299 | defaultValue = option[prop];
|
300 | }
|
301 | next();
|
302 | }
|
303 | }, function() {
|
304 |
|
305 | option.default = defaultValue;
|
306 | delete option.altDefault;
|
307 |
|
308 | var validator = option.validator;
|
309 | option.validator = function(line, next) {
|
310 | if (line === '?') {
|
311 | return next(false);
|
312 | } else if (validator) {
|
313 | if (validator.test) {
|
314 | return next(validator.test(line));
|
315 | } else if (typeof validator === 'function') {
|
316 | return validator.length < 2 ? next(validator(line)) : validator(line, next);
|
317 | }
|
318 | }
|
319 | next(true);
|
320 | };
|
321 |
|
322 | prompt.start();
|
323 | prompt.getInput(option, function(err, line) {
|
324 | if (err) { return done(err); }
|
325 | option.validator = validator;
|
326 | result[option.name] = line;
|
327 | done();
|
328 | });
|
329 | });
|
330 | }, function(err) {
|
331 |
|
332 | if (/n/i.test(result.ANSWERS_VALID)) {
|
333 |
|
334 | prompt.pause();
|
335 |
|
336 | delete result.ANSWERS_VALID;
|
337 |
|
338 | grunt.utils.async.forEachSeries(Object.keys(result), function(name, next) {
|
339 |
|
340 | if (sanitize[name]) {
|
341 | sanitize[name](result[name], result, function(err, value) {
|
342 | if (err) {
|
343 | result[name] = err;
|
344 | } else if (arguments.length === 2) {
|
345 | result[name] = value === 'none' ? '' : value;
|
346 | }
|
347 | next();
|
348 | });
|
349 | } else {
|
350 | if (result[name] === 'none') { result[name] = ''; }
|
351 | next();
|
352 | }
|
353 | }, function(err) {
|
354 |
|
355 | grunt.log.writeln();
|
356 | done(err, result);
|
357 | });
|
358 | } else {
|
359 |
|
360 | options.slice(0, -1).forEach(function(option) {
|
361 | option.default = result[option.name];
|
362 | });
|
363 |
|
364 | ask();
|
365 | }
|
366 | });
|
367 | }());
|
368 | });
|
369 |
|
370 |
|
371 |
|
372 |
|
373 |
|
374 | var prompts = {
|
375 | name: {
|
376 | message: 'Project name',
|
377 | default: function(value, data, done) {
|
378 | var types = ['javascript', 'js'];
|
379 | if (data.type) { types.push(data.type); }
|
380 | var type = '(?:' + types.join('|') + ')';
|
381 |
|
382 |
|
383 |
|
384 | var re = new RegExp('^' + type + '[\\-\\._]?|(?:[\\-\\._]?' + type + ')?(?:[\\-\\._]?js)?$', 'ig');
|
385 |
|
386 | var name = path.basename(process.cwd()).replace(re, '');
|
387 |
|
388 | name = name.replace(/[^\w\-\.]/g, '');
|
389 | done(null, name);
|
390 | },
|
391 | validator: /^[\w\-\.]+$/,
|
392 | warning: 'Must be only letters, numbers, dashes, dots or underscores.',
|
393 | sanitize: function(value, data, done) {
|
394 |
|
395 | data.js_safe_name = value.replace(/[\W_]+/g, '_').replace(/^(\d)/, '_$1');
|
396 |
|
397 | done();
|
398 | }
|
399 | },
|
400 | title: {
|
401 | message: 'Project title',
|
402 | default: function(value, data, done) {
|
403 | var title = data.name || '';
|
404 | title = title.replace(/[\W_]+/g, ' ');
|
405 | title = title.replace(/\w+/g, function(word) {
|
406 | return word[0].toUpperCase() + word.slice(1).toLowerCase();
|
407 | });
|
408 | done(null, title);
|
409 | },
|
410 | warning: 'May consist of any characters.'
|
411 | },
|
412 | description: {
|
413 | message: 'Description',
|
414 | default: 'The best project ever.',
|
415 | warning: 'May consist of any characters.'
|
416 | },
|
417 | version: {
|
418 | message: 'Version',
|
419 | default: function(value, data, done) {
|
420 |
|
421 | grunt.utils.spawn({
|
422 | cmd: 'git',
|
423 | args: ['describe', '--tags'],
|
424 | fallback: ''
|
425 | }, function(err, result, code) {
|
426 | result = result.split('-')[0];
|
427 | done(null, semver.valid(result) || '0.1.0');
|
428 | });
|
429 | },
|
430 | validator: semver.valid,
|
431 | warning: 'Must be a valid semantic version (semver.org).'
|
432 | },
|
433 | repository: {
|
434 | message: 'Project git repository',
|
435 | default: function(value, data, done) {
|
436 |
|
437 | grunt.helper('git_origin', function(err, result) {
|
438 | if (err) {
|
439 |
|
440 | result = 'git://github.com/' + (process.env.USER || process.env.USERNAME || '???') + '/' +
|
441 | data.name + '.git';
|
442 | } else {
|
443 | result = result.replace(/^git@([^:]+):/, 'git://$1/');
|
444 | }
|
445 | done(null, result);
|
446 | });
|
447 | },
|
448 | sanitize: function(value, data, done) {
|
449 |
|
450 | var repo = grunt.helper('github_web_url', data.repository);
|
451 | var parts;
|
452 | if (repo != null) {
|
453 | parts = repo.split('/');
|
454 | data.git_user = parts[parts.length - 2];
|
455 | data.git_repo = parts[parts.length - 1];
|
456 | done();
|
457 | } else {
|
458 |
|
459 | grunt.utils.spawn({
|
460 | cmd: 'git',
|
461 | args: ['config', '--get', 'github.user'],
|
462 | fallback: ''
|
463 | }, function(err, result, code) {
|
464 | data.git_user = result || process.env.USER || process.env.USERNAME || '???';
|
465 | data.git_repo = path.basename(process.cwd());
|
466 | done();
|
467 | });
|
468 | }
|
469 | },
|
470 | warning: 'Should be a public git:// URI.'
|
471 | },
|
472 | homepage: {
|
473 | message: 'Project homepage',
|
474 |
|
475 | default: function(value, data, done) {
|
476 | done(null, grunt.helper('github_web_url', data.repository) || 'none');
|
477 | },
|
478 | warning: 'Should be a public URL.'
|
479 | },
|
480 | bugs: {
|
481 | message: 'Project issues tracker',
|
482 |
|
483 | default: function(value, data, done) {
|
484 | done(null, grunt.helper('github_web_url', data.repository, 'issues') || 'none');
|
485 | },
|
486 | warning: 'Should be a public URL.'
|
487 | },
|
488 | licenses: {
|
489 | message: 'Licenses',
|
490 | default: 'MIT',
|
491 | validator: /^[\w\-]+(?:\s+[\w\-]+)*$/,
|
492 | warning: 'Must be zero or more space-separated licenses. Built-in ' +
|
493 | 'licenses are: ' + availableLicenses().join(' ') + ', but you may ' +
|
494 | 'specify any number of custom licenses.',
|
495 |
|
496 | sanitize: function(value, data, done) { done(value.split(/\s+/)); }
|
497 | },
|
498 | author_name: {
|
499 | message: 'Author name',
|
500 | default: function(value, data, done) {
|
501 |
|
502 | grunt.utils.spawn({
|
503 | cmd: 'git',
|
504 | args: ['config', '--get', 'user.name'],
|
505 | fallback: 'none'
|
506 | }, done);
|
507 | },
|
508 | warning: 'May consist of any characters.'
|
509 | },
|
510 | author_email: {
|
511 | message: 'Author email',
|
512 | default: function(value, data, done) {
|
513 |
|
514 | grunt.utils.spawn({
|
515 | cmd: 'git',
|
516 | args: ['config', '--get', 'user.email'],
|
517 | fallback: 'none'
|
518 | }, done);
|
519 | },
|
520 | warning: 'Should be a valid email address.'
|
521 | },
|
522 | author_url: {
|
523 | message: 'Author url',
|
524 | default: 'none',
|
525 | warning: 'Should be a public URL.'
|
526 | },
|
527 | jquery_version: {
|
528 | message: 'Required jQuery version',
|
529 | default: '*',
|
530 | warning: 'Must be a valid semantic version range descriptor.'
|
531 | },
|
532 | node_version: {
|
533 | message: 'What versions of node does it run on?',
|
534 |
|
535 | default: '>= 0.6.0',
|
536 | warning: 'Must be a valid semantic version range descriptor.'
|
537 | },
|
538 | main: {
|
539 | message: 'Main module/entry point',
|
540 | default: function(value, data, done) {
|
541 | done(null, 'lib/' + data.name);
|
542 | },
|
543 | warning: 'Must be a path relative to the project root.'
|
544 | },
|
545 | bin: {
|
546 | message: 'CLI script',
|
547 | default: function(value, data, done) {
|
548 | done(null, 'bin/' + data.name);
|
549 | },
|
550 | warning: 'Must be a path relative to the project root.'
|
551 | },
|
552 | npm_test: {
|
553 | message: 'Npm test command',
|
554 | default: 'grunt test',
|
555 | warning: 'Must be an executable command.'
|
556 | },
|
557 | grunt_version: {
|
558 | message: 'What versions of grunt does it require?',
|
559 | default: '~' + grunt.version,
|
560 | warning: 'Must be a valid semantic version range descriptor.'
|
561 | }
|
562 | };
|
563 |
|
564 |
|
565 | grunt.registerHelper('prompt_for_obj', function() {
|
566 | return prompts;
|
567 | });
|
568 |
|
569 |
|
570 | grunt.registerHelper('prompt_for', function(name, altDefault) {
|
571 |
|
572 | var option = grunt.utils._.clone(prompts[name]);
|
573 | option.name = name;
|
574 |
|
575 | var defaults = grunt.task.readDefaults('init/defaults.json');
|
576 | if (name in defaults) {
|
577 |
|
578 | option.default = defaults[name];
|
579 | } else if (arguments.length === 2) {
|
580 |
|
581 | option.altDefault = altDefault;
|
582 | }
|
583 | return option;
|
584 | });
|
585 |
|
586 |
|
587 | grunt.registerHelper('git_origin', function(done) {
|
588 | grunt.utils.spawn({
|
589 | cmd: 'git',
|
590 | args: ['remote', '-v']
|
591 | }, function(err, result, code) {
|
592 | var re = /^origin\s/;
|
593 | var lines;
|
594 | if (!err) {
|
595 | lines = result.split('\n').filter(re.test, re);
|
596 | if (lines.length > 0) {
|
597 | done(null, lines[0].split(/\s/)[1]);
|
598 | return;
|
599 | }
|
600 | }
|
601 | done(true, 'none');
|
602 | });
|
603 | });
|
604 |
|
605 |
|
606 | var githubWebUrlRe = /^.+(?:@|:\/\/)(github.com)[:\/](.+?)(?:\.git|\/)?$/;
|
607 | grunt.registerHelper('github_web_url', function(uri, suffix) {
|
608 | var matches = githubWebUrlRe.exec(uri);
|
609 | if (!matches) { return null; }
|
610 | var url = 'https://' + matches[1] + '/' + matches[2];
|
611 | if (suffix) {
|
612 | url += '/' + suffix.replace(/^\//, '');
|
613 | }
|
614 | return url;
|
615 | });
|
616 |
|
617 | };
|