UNPKG

15.9 kBJavaScriptView Raw
1"use strict";
2
3require( "colors" );
4const
5 FS = require( "fs" ),
6 RL = require( "readline" ),
7 Tpl = require( "./template" ),
8 Path = require( "path" ),
9 Input = require( 'readline-sync' ),
10 Fatal = require( "./fatal" ),
11 Package = require( "./package" ),
12 Template = require( "./template" ),
13 PathUtils = require( "./pathutils" ),
14 ChildProcess = require( 'child_process' );
15
16
17var OPTIONS = {};
18
19exports.start = function ( pkg ) {
20 if ( !isInEmptyFolder() ) {
21 console.log();
22 yesno( "Are you sure you still want to continue in this non empty folder?\n" +
23 "This will DELETE all the files!".bgRed.white + " ", stepStart );
24 } else {
25 stepStart();
26 };
27};
28
29
30function stepStart() {
31 try {
32 if ( !isGitInstalled() ) return;
33 OPTIONS = {};
34 menu( [
35 "Create an empty fresh new project.",
36 "Create from existing sources."
37 ], function ( choice ) {
38 if ( choice == 1 ) onMenuProjectType();
39 else onExistingSources();
40 } );
41 } catch ( ex ) {
42 fatal( "Fatal error in function `stepStart`!\n" + JSON.stringify( ex, null, ' ' ) );
43 }
44};
45
46
47function onExistingSources() {
48 try {
49 input( "Folder where your HTML files are: ", function ( path ) {
50 path = Path.resolve( path );
51 if ( !FS.existsSync( path ) ) {
52 fatal( "I can't find this folder:\n" + path );
53 } else {
54 var stat = FS.statSync( path );
55 if ( !stat.isDirectory() ) {
56 fatal( "Sorry but this is not a folder:\n" + path );
57 } else {
58 OPTIONS.sources = path;
59 onMenuProjectType();
60 }
61 }
62 } );
63 } catch ( ex ) {
64 fatal( "Fatal error in function `onExistingSources`!\n" + JSON.stringify( ex, null, ' ' ) );
65 }
66}
67
68
69function onMenuProjectType() {
70 try {
71 var defaults = { name: '', desc: '', author: '' };
72 var packageFilename = "./package.json";
73 if ( FS.existsSync( packageFilename ) ) {
74 try {
75 var pkg = JSON.parse( FS.readFileSync( packageFilename ) );
76 defaults.name = pkg.name;
77 defaults.desc = pkg.description;
78 defaults.author = pkg.author;
79 } catch ( ex ) {
80 console.error( "For information, your current `package.json` file is not parsable!".red );
81 }
82 }
83
84 inputs( {
85 name: [ "Project's name: ", defaults.name ],
86 desc: [ "Project's description: ", defaults.desc ],
87 author: [ "Author (should be github username if you use github): ", defaults.author ]
88 }, function ( values ) {
89 copyToOptions( values );
90 OPTIONS.url = "https://github.com/" + OPTIONS.author + "/" + OPTIONS.name + ".git";
91 OPTIONS.bugs = "https://github.com/" + OPTIONS.author + "/" + OPTIONS.name + "/issues";
92 OPTIONS.homepage = "https://" + OPTIONS.author + ".github.io/" + OPTIONS.name;
93 var versionParts = Package.version.split( '.' );
94 while ( versionParts.length > 2 ) {
95 versionParts.pop();
96 }
97 OPTIONS.version = versionParts.join( '.' );
98 stepSelectType( function () {
99 yesno(
100 "Do you want to use GitHub? ",
101 stepGithub,
102 function () {
103 console.log();
104 console.log( "Initializing git..." );
105 exec( "git init" );
106 exec( "git add .gitignore" );
107 exec( "git add . -A" );
108 exec( 'git commit -m "First commit."' );
109 exec( 'mkdir submodules' );
110 exec(
111 'git submodule add https://github.com/tolokoban/toloframework.git submodules/tfw'
112 );
113 stepEnd();
114 }
115 );
116 } );
117 } );
118 } catch ( ex ) {
119 fatal( "Fatal error in function `onMenuProjectType`!\n" + JSON.stringify( ex, null, ' ' ) );
120 }
121}
122
123
124function stepGithub() {
125 try {
126 console.log( "Deleting all files in the current folder..." );
127 cleanDir( "." );
128 console.log( "Cloning GitHub's repository..." );
129 exec( "git clone " + OPTIONS.url + " ." );
130 console.log( "Preparing branch gh-pages...".cyan );
131 var branches = exec( "git branch" );
132 console.log( "Preparing pg-pages...".cyan );
133 if ( branches.indexOf( " gh-pages" ) == -1 ) {
134 exec( "git branch gh-pages" );
135 exec( "git push -u origin gh-pages" );
136 }
137 if ( !FS.existsSync( "www" ) ) {
138 FS.mkdirSync( "www" );
139 } else {
140 PathUtils.rmdir( "./www" );
141 }
142 exec( "git clone " + OPTIONS.url + " ./www" );
143 exec( "cd www && git checkout gh-pages" );
144 stepEnd();
145 } catch ( ex ) {
146 fatal( "Fatal error in function `stepGithub`!\n" + JSON.stringify( ex, null, ' ' ) );
147 }
148}
149
150function copyExistingSources() {
151 try {
152 console.log( "Copying existing file..." );
153 copyFile( OPTIONS.sources, "./src" );
154 } catch ( ex ) {
155 fatal( "Fatal error in function `copyExistingSources`!\n" + JSON.stringify( ex, null, ' ' ) );
156 }
157}
158
159function stepEnd() {
160 try {
161 console.log( "package.json" );
162 FS.writeFileSync( "./package.json", Template.file( "package.json", OPTIONS ).out );
163 console.log( "karma.conf.js" );
164 FS.writeFileSync( "./karma.conf.js", Template.file( "karma.conf.js", OPTIONS ).out );
165 console.log( ".gitignore" );
166 FS.writeFileSync( "./.gitignore", `
167*~
168.#*
169tmp/
170www/
171node_modules/
172spec/mod/
173` );
174 console.log( "src" );
175 Template.files( "src", "./src" );
176 console.log( "Downloading external modules..." );
177 exec( "npm update" );
178 if ( OPTIONS.sources ) copyExistingSources();
179 exec( 'git add . -A' );
180 exec( 'git commit -m "tfw init"' );
181
182 console.log();
183 console.log( "------------------------------------------------------------" );
184 console.log();
185 console.log( "The initialization is done." );
186 console.log( "Your compiled project will be build in your `www` folder." );
187 console.log();
188 console.log( "To start automated tests, please type:" );
189 console.log( "> " + "npm test".yellow );
190 console.log();
191 console.log( "To perform a full clean-up, please type:" );
192 console.log( "> " + "npm run clean".yellow );
193 console.log();
194 console.log( "To build in DEBUG mode, please type:" );
195 console.log( "> " + "npm run debug".yellow );
196 console.log( "If you want the files to be watched during DEBUG mode, please type:" );
197 console.log( "> " + "npm run watch".yellow );
198 console.log();
199 console.log( "To build in RELEASE mode, please type:" );
200 console.log( "> " + "npm run release".yellow );
201 console.log();
202 } catch ( ex ) {
203 fatal( "Fatal error in function `stepEnd`!\n" + JSON.stringify( ex, null, ' ' ) );
204 }
205}
206
207
208function stepSelectType( nextStep ) {
209 try {
210 menu( [ "Browser's application", "Node-webkit's application" ], function ( v ) {
211 if ( v == 1 ) {
212 OPTIONS.type = "web";
213 } else {
214 OPTIONS.type = "desktop";
215 }
216 nextStep();
217 } );
218 } catch ( ex ) {
219 fatal( "Fatal error in function `stepSelectType`!\n" + JSON.stringify( ex, null, ' ' ) );
220 }
221}
222
223/**
224 * Display a list of items and ask the user to select one.
225 * Each item is numbered and the menu is displayed again if the user
226 * enter a non-existing value.
227 * The function `nextStep` is called with the choice as a number.
228 */
229function menu( items, nextStep ) {
230 const rl = RL.createInterface( {
231 input: process.stdin,
232 output: process.stdout
233 } );
234 console.log();
235 items.forEach( function ( item, idx ) {
236 var out = idx < 10 ? ' ' : '';
237 out += ( "" + ( 1 + idx ) + ") " ).yellow;
238 out += item;
239 console.log( out );
240 } );
241 console.log();
242 rl.question( "Your choice: ", ( ans ) => {
243 rl.close();
244 var choice = parseInt( ans );
245 if ( isNaN( ans ) || ans < 1 || ans > items.length ) {
246 menu( items, nextStep );
247 } else {
248 console.log();
249 nextStep( choice );
250 }
251 } );
252}
253
254function yesno( caption, yesStep, noStep, defaultValue ) {
255 if ( typeof defaultValue === 'undefined' ) defaultValue = 'N';
256 defaultValue = defaultValue.trim().toUpperCase();
257
258 if ( Array.isArray( caption ) ) caption = caption[ 0 ];
259 input( [ caption, defaultValue ], function ( ans ) {
260 ans = ans.trim().toUpperCase();
261 if ( ans == "" ) ans = defaultValue;
262 if ( ans == 'Y' ) yesStep();
263 else if ( ans == 'N' ) {
264 if ( typeof noStep === 'function' ) {
265 noStep();
266 }
267 } else {
268 console.log( "Please answer Y or N!".red );
269 yesno( caption, yesStep, noStep, defaultValue );
270 }
271 } );
272}
273
274function input( caption, nextStep ) {
275 if ( typeof caption !== 'string' && !Array.isArray( caption ) ) {
276 inputs( caption, nextStep );
277 } else {
278 const rl = RL.createInterface( {
279 input: process.stdin,
280 output: process.stdout
281 } );
282 var text, defaultValue = '';
283 if ( Array.isArray( caption ) ) {
284 if ( caption.length > 1 ) {
285 defaultValue = caption[ 1 ];
286 }
287 if ( typeof defaultValue !== 'string' ) defaultValue = '';
288 text = caption[ 0 ];
289 if ( defaultValue != '' ) {
290 text += ( "[" + defaultValue + "] " ).gray;
291 }
292 } else {
293 text = caption;
294 }
295 rl.question( text, ( ans ) => {
296 rl.close();
297 if ( ans.trim() == '' ) ans = defaultValue;
298 nextStep( ans );
299 } );
300 }
301}
302
303function inputs( captions, nextStep ) {
304 var items = [];
305 var k, v;
306 for ( k in captions ) {
307 v = captions[ k ];
308 items.push( [ k, v ] );
309 }
310 // The resuls of the inputs are stored here.
311 var values = {};
312
313 var callback = function () {
314 if ( items.length == 0 ) {
315 nextStep( values );
316 } else {
317 var item = items.shift();
318 input( item[ 1 ], function ( value ) {
319 values[ item[ 0 ] ] = value;
320 callback();
321 } );
322 }
323 };
324
325 callback();
326}
327
328/**
329 * Check if we are in an empty folder.
330 */
331function isInEmptyFolder() {
332 var files = FS.readdirSync( '.' );
333 try {
334 if ( files.length == 0 ) return true;
335 console.log( Fatal.format(
336 "You should be in an empty folder to create a new project!\n" +
337 "If you continue, all the files in this folder will be DELETED!"
338 ) );
339 console.log( "\nWe suggest that you create an fresh new directory, like this:" );
340 console.log( "\n> " + "mkdir my-project-folder".yellow.italic );
341 console.log( "> " + "cd my-project-folder".yellow.italic );
342 console.log( "> " + "tfw init".yellow.italic );
343 return false;
344 } catch ( ex ) {
345 fatal( "Fatal error in function `files`!\n" + JSON.stringify( ex, null, ' ' ) );
346 }
347}
348
349/**
350 * Check if `git` is installed on this system.
351 */
352function isGitInstalled() {
353 var result = exec( "git --version", true );
354 try {
355 if ( !result || result.indexOf( "git" ) < 0 || result.indexOf( "version" ) < 0 ) {
356 console.log( Fatal.format(
357 "`git` is required by the ToloFrameWork!\n" +
358 "Please install it:" ) );
359 console.log( "\n> " + "sudo apt-get install git".yellow.italic );
360 return false;
361 }
362 return true;
363 } catch ( ex ) {
364 fatal( "Fatal error in function `result`!\n" + JSON.stringify( ex, null, ' ' ) );
365 }
366}
367
368function copyToOptions( values ) {
369 var k, v;
370 try {
371 for ( k in values ) {
372 v = values[ k ];
373 OPTIONS[ k ] = v;
374 }
375 return OPTIONS;
376 } catch ( ex ) {
377 fatal( "Fatal error in function `k`!\n" + JSON.stringify( ex, null, ' ' ) );
378 }
379}
380
381function exec( cmd, silent ) {
382 try {
383 if ( !silent ) {
384 console.log( "> " + cmd.yellow );
385 }
386 return ChildProcess.execSync( cmd ).toString();
387 } catch ( ex ) {
388 console.log( ( "" + ex ).red );
389 }
390}
391
392
393/**
394 * Remove all the files and folder in `path`, but not `path` itself.
395 */
396function cleanDir( path ) {
397 var files = FS.readdirSync( path );
398 try {
399 files.forEach( function ( file ) {
400 var fullpath = Path.join( path, file );
401 if ( !FS.existsSync( fullpath ) ) return;
402 var stat = FS.statSync( fullpath );
403 try {
404 if ( stat.isDirectory() ) PathUtils.rmdir( fullpath );
405 else FS.unlinkSync( fullpath );
406 } catch ( ex ) {
407 console.error( "Unable to delete `" + fullpath + "`!" );
408 console.error( ex );
409 }
410 } );
411 } catch ( ex ) {
412 fatal( "Fatal error in function `files`!\n" + JSON.stringify( ex, null, ' ' ) );
413 }
414}
415
416
417/**
418 * Show a nice error message.
419 */
420function fatal( msg ) {
421 console.log( Fatal.format( msg ) );
422}
423
424
425
426var BUFFER = new Buffer( 64 * 1024 );
427
428function copyFile( src, dst ) {
429 try {
430 if ( !FS.existsSync( src ) ) {
431 fatal( "Unable to copy missing file: " + src + "\ninto: " + dst );
432 }
433 var stat = FS.statSync( src );
434 if ( stat.isDirectory() ) {
435 // We need to copy a whole directory.
436 if ( FS.existsSync( dst ) ) {
437 // Check if the destination is a directory.
438 stat = FS.statSync( dst );
439 if ( !stat.isDirectory() ) {
440 fatal( "Destination is not a directory: \"" + dst +
441 "\"!\nSource is \"" + src + "\"." );
442 }
443 } else {
444 // Make destination directory.
445 PathUtils.mkdir( dst );
446 }
447 var files = FS.readdirSync( src );
448 files.forEach(
449 function ( filename ) {
450 copyFile(
451 Path.join( src, filename ),
452 Path.join( dst, filename )
453 );
454 }
455 );
456 return;
457 }
458
459 var bytesRead, pos, rfd, wfd;
460 PathUtils.mkdir( Path.dirname( dst ) );
461 try {
462 rfd = FS.openSync( src, "r" );
463 } catch ( ex ) {
464 fatal( "Unable to open file \"" + src + "\" for reading!\n" + JSON.stringify( ex, null, ' ' ) );
465 }
466 try {
467 wfd = FS.openSync( dst, "w" );
468 } catch ( ex ) {
469 fatal( "Unable to open file \"" + dst + "\" for writing!\n" + JSON.stringify( ex, null, ' ' ) );
470 }
471 bytesRead = 1;
472 pos = 0;
473 while ( bytesRead > 0 ) {
474 try {
475 bytesRead = FS.readSync( rfd, BUFFER, 0, 64 * 1024, pos );
476 } catch ( ex ) {
477 fatal( "Unable to read file \"" + src + "\"!\n" + JSON.stringify( ex, null, ' ' ) );
478 }
479 FS.writeSync( wfd, BUFFER, 0, bytesRead );
480 pos += bytesRead;
481 }
482 FS.closeSync( rfd );
483 FS.closeSync( wfd );
484 } catch ( ex ) {
485 fatal( "Fatal error in function `copyFile`!\n" + JSON.stringify( ex, null, ' ' ) );
486 }
487};