UNPKG

46.8 kBJavaScriptView Raw
1"use strict";
2
3module.exports = {
4
5 /**
6 * @param prjDir root directory of the project. It is where we can find `project.tfw.json`.
7 * @return instance of the class `Project`.
8 */
9 createProject,
10 ERR_WIDGET_TRANSFORMER: 1,
11 ERR_WIDGET_NOT_FOUND: 2,
12 ERR_WIDGET_TOO_DEEP: 3,
13 ERR_FILE_NOT_FOUND: 4
14};
15
16/**
17 * @see Project.compile
18 */
19const
20 CompilerHTML2 = require( "./compiler-html2" ),
21 CompilerPHP = require( "./compiler-php" ),
22 ConfigurationLoader = require( "./configuration-loader" ),
23 Fatal = require( "./fatal" ),
24 FS = require( "fs" ),
25 Path = require( "path" ),
26 PathUtils = require( "./pathutils" ),
27 Source = require( "./source" ),
28 Template = require( "./template" ),
29 Tree = require( "./htmltree" ),
30 Util = require( "./util" ),
31 WebWorkers = require( "./web-workers" ),
32 WidgetUtil = require( "../ker/wdg/util.js" );
33
34const
35 ONE_KILOBYTES = 1024,
36 TWO_KILOBYTES = 2048,
37 NOT_FOUND_IN_ARRAY = -1;
38
39/**
40 * @class project
41 * @param {string} prjDir root directory of the project. It is where we can find `project.tfw.json`.
42 */
43function Project( prjDir ) {
44
45 initializeProjectDirectories( this, prjDir );
46 initializedPrivateVariables( this );
47
48 this.Util = WidgetUtil;
49
50 const cfg = ConfigurationLoader.parse( prjDir );
51 this._config = cfg;
52
53 this.checkProjectOutputFolder();
54 this.findExtraModules();
55
56 this.mkdir( this.srcPath( "mod" ) );
57 this.mkdir( this.srcPath( "wdg" ) );
58
59 this._type = cfg.tfw.compile.type;
60
61 if ( this._type === 'web' ) {
62 this._config.reservedModules = [];
63 } else {
64 this._config.reservedModules = [
65 "fs", "path", "process", "child_process", "cluster", "http", "os",
66 "crypto", "dns", "domain", "events", "https", "net", "readline",
67 "stream", "string_decoder", "tls", "dgram", "util", "vm", "zlib"
68 ];
69 }
70 this._modulesPath.forEach( function forEachModulePath( path ) {
71 console.log( `External lib: ${path.bold}` );
72 } );
73}
74
75
76/**
77 * Modules can be foind in the `./src/mod` folder, in the toloFrameWork standard library
78 * and in all folders defined in the config attribute `tfw.modules` which is an array.
79 * @member Project.findExtraModules
80 * @private
81 * @return {undefined}
82 */
83Project.prototype.findExtraModules = function findExtraModules() {
84 const
85 that = this,
86 cfg = this._config;
87 cfg.tfw.modules.forEach( function forEachModule( _item ) {
88 const item = Path.resolve( that._prjDir, _item );
89 if ( FS.existsSync( item ) ) {
90 that._modulesPath.push( item );
91 console.log( `Extra modules from: ${item}!` );
92 } else {
93 that.fatal( `Unable to find module directory:\n${item}` );
94 }
95 } );
96};
97
98
99/**
100 * @member Project.checkProjectOutputFolder
101 * @private
102 * @return {undefined}
103 */
104Project.prototype.checkProjectOutputFolder = function checkProjectOutputFolder() {
105 const cfg = this._config;
106 // Is there an output specified in config file?
107 if ( cfg.tfw && cfg.tfw.output ) {
108 this._wwwDir = this.prjPath( cfg.tfw.output );
109 if ( !FS.existsSync( this._wwwDir ) ) {
110 this.fatal( `Output folder does not exist: "${this._wwwDir}"!` );
111 }
112 }
113 console.log( `Output folder: ${this._wwwDir.yellow}` );
114};
115
116
117/**
118 * @param {object} project - Reference to the current Project object.
119 * @param {string} prjDir - Root directory of this project.
120 * @returns {undefined}
121 */
122function initializeProjectDirectories( project, prjDir ) {
123 project._prjDir = Path.resolve( prjDir );
124 project._libDir = Path.resolve( Path.join( __dirname, "../ker" ) );
125 project._tplDir = Path.resolve( Path.join( __dirname, "../tpl" ) );
126 project._srcDir = project.mkdir( prjDir, "src" );
127 project._docDir = project.mkdir( prjDir, "doc" );
128 project._tmpDir = project.mkdir( prjDir, "tmp" );
129 project._wwwDir = project.mkdir( prjDir, "www" );
130 Util.cleanDir( project.wwwPath( 'js' ), true );
131 Util.cleanDir( project.wwwPath( 'css' ), true );
132}
133
134/**
135 * Create the file `mod/$.js` only if `package.json` is newer.
136 * This special module must never be wrapped in a module.
137 * Otherwise, it will require itself and lead to an infinite loop.
138 * @param {object} project - Project instance.
139 * @param {object} cfg - Config parsed from `package.json`.
140 * @param {object} options - Building options.
141 * @returns {undefined}
142 */
143function createModule$IfNeeded( project, cfg, options ) {
144 const file = project.srcPath( "mod/$.js" );
145 if ( !PathUtils.isNewer( project.prjPath( 'package.json' ), file ) ) {
146 // There is nothing to do until `$.js` is not older than `package.json`.
147 return;
148 }
149 const moduleContent = getModuleContentFor$( cfg );
150
151 if ( cfg.tfw.consts ) {
152 if ( cfg.tfw.consts.all ) {
153 Object.keys( cfg.tfw.consts.all ).forEach( ( key ) => {
154 const val = cfg.tfw.consts.all[ key ];
155 moduleContent.consts[ key ] = val;
156 } );
157 }
158 if ( options.dev ) {
159 if ( cfg.tfw.consts.debug ) {
160 Object.keys( cfg.tfw.consts.debug ).forEach( ( key ) => {
161 const val = cfg.tfw.consts.debug[ key ];
162 moduleContent.consts[ key ] = val;
163 } );
164 }
165 } else if ( cfg.tfw.consts.release ) {
166 Object.keys( cfg.tfw.consts.debug ).forEach( ( key ) => {
167 const val = cfg.tfw.consts.release[ key ];
168 moduleContent.consts[ key ] = val;
169 } );
170 }
171 console.log( "Constants: ".bold.yellow + JSON.stringify( moduleContent.consts, null, ' ' ) );
172 }
173 const contentOfModule$ = FS.readFileSync( Path.join( project._tplDir, "$.js" ) );
174 PathUtils.file(
175 file,
176 `exports.config=${JSON.stringify( moduleContent )};\n${contentOfModule$}`
177 );
178}
179
180/**
181 * @param {string} filename - ...
182 * @param {string} content - ...
183 * @param {string} _path - ...
184 *
185 * @return {boolean} `true` if the file needed to be flushed.
186 */
187Project.prototype.flushContent = function flushContent( filename, content, _path ) {
188 const
189 path = typeof _path !== "undefined" ? _path : '.',
190 filepath = Path.join( path, filename );
191
192 if ( this.hasAlreadyBeenFlushed( filepath ) ) {
193 return false;
194 }
195
196 if ( filepath.indexOf( '@' ) !== NOT_FOUND_IN_ARRAY ) {
197 const
198 DECIMALS_FOR_SMALL_NUMBER = 3,
199 decimals = content.length < TWO_KILOBYTES ? DECIMALS_FOR_SMALL_NUMBER : 0,
200 fileSize = ( content.length / ONE_KILOBYTES ).toFixed( decimals );
201 console.log( `>>> ${filepath.cyan} (${fileSize.yellow} kb) ${this.wwwPath(filepath).grey}` );
202 }
203
204 const wwwFilePath = this.wwwPath( filepath );
205
206 this.mkdir( Path.dirname( wwwFilePath ) );
207 FS.writeFile( wwwFilePath, content, ( err ) => {
208 if ( err ) {
209 console.info( "[project] content=...", content );
210 Fatal.fire(
211 `Unable to write the file: "${wwwFilePath}"\n\n!${err}`,
212 Fatal.UNDEFINED,
213 "project.flushContent"
214 );
215 }
216 } );
217 return true;
218};
219
220/**
221 * @param {string} fileFullPath . Full path of the file we want to know if it has already been flushed.
222 * @returns {boolean} Wether this file has already been flushed.
223 */
224Project.prototype.hasAlreadyBeenFlushed = function hasAlreadyBeenFlushed( fileFullPath ) {
225 return this._flushedContent.indexOf( fileFullPath ) !== NOT_FOUND_IN_ARRAY;
226};
227
228/**
229 * Compile every `*.html` file found in _srcDir_.
230 * @param {object} _options - Options for debug, release, ...
231 * @returns {array} CompiledFiles.
232 */
233Project.prototype.compile = function ( _options ) {
234 const
235 options = typeof _options !== 'undefined' ? _options : {},
236 that = this,
237 cfg = this._config;
238
239 options.config = this._config;
240 this.options = options;
241
242 createModule$IfNeeded( this, cfg, options );
243
244 CompilerHTML2.initialize( this );
245
246 // List of modules for doc.
247 this._modulesList = [];
248 const compiledFiles = compileHtmlFiles(
249 this.findHtmlFiles(), options
250 );
251
252 // Copying resources.
253 copyResources( this, compiledFiles, cfg );
254 // WebWorkers
255 WebWorkers.compile( this, compiledFiles );
256
257 // Look at `manifest.webapp` (FxOS) or `package.json` (NWJS).
258 if ( this._type === 'nodewebkit' ) {
259 ( function () {
260 // For NWJS, we have to copy `package.json`.
261 var content = PathUtils.file( that.srcPath( "../package.json" ) );
262 var data = JSON.parse( content );
263 if ( typeof data.main !== 'string' ) {
264 data.main = "index.html";
265 }
266 PathUtils.file( that.wwwPath( "package.json" ), JSON.stringify( data, null, 2 ) );
267 } )();
268 } else {
269 ( function () {
270 var manifest = that.srcPath( "manifest.webapp" );
271 var content = PathUtils.file( manifest );
272 var data;
273 try {
274 data = JSON.parse( content );
275 } catch ( ex ) {
276 data = null;
277 }
278 if ( !data || typeof data !== 'object' ) {
279 data = {
280 launch_path: '/index.html',
281 developer: {
282 name: cfg.author,
283 url: cfg.homepage
284 },
285 icons: {
286 "128": "/icon-128.png",
287 "512": "/icon-512.png"
288 }
289 };
290 }
291 data.name = cfg.name;
292 data.version = cfg.version;
293 data.description = cfg.description;
294 PathUtils.file( manifest, JSON.stringify( data, null, 2 ) );
295 PathUtils.file( that.wwwPath( "manifest.webapp" ), JSON.stringify( data, null, 2 ) );
296 // Copy the icons.
297 if ( typeof data.icons === 'object' ) {
298 var key, val, icon;
299 for ( key in data.icons ) {
300 icon = data.icons[ key ];
301 val = that.srcOrLibPath( icon );
302 if ( !val ) {
303 console.log( ' Warning! '.yellowBG + "Missing icon: " + icon.bold );
304 } else {
305 that.copyFile( val, that.wwwPath( icon ) );
306 }
307 }
308 }
309 } )();
310 }
311
312 this._compiledFiles = compiledFiles;
313 return compiledFiles;
314};
315
316/**
317 * If a resource file changes, we have to touch the corresponding module's JS file.
318 */
319Project.prototype.cascadingTouch = function ( path ) {
320 var srcPath = this.srcPath( 'mod' );
321 path = Path.normalize( path );
322 // We are looking for files in resource folders.
323 if ( PathUtils.isDirectory( path ) ) return;
324 if ( path.length < srcPath.length ) return;
325 if ( path.substr( 0, srcPath.length ) != srcPath ) return;
326
327 // Path of the corresponding JS module.
328 var modulePath;
329 // Up to the prent dir.
330 path = Path.dirname( path );
331 while ( path.length > srcPath ) {
332 modulePath = path + ".js";
333 if ( Path.existsSync( modulePath ) ) {
334 PathUtils.touch( modulePath );
335 return;
336 }
337 }
338};
339
340
341/**
342 * @return void
343 */
344Project.prototype.getCompiledFiles = function () {
345 return this._compiledFiles;
346};
347
348
349/**
350 * @return void
351 */
352Project.prototype.services = function ( options ) {
353 console.log( "Adding services...".cyan );
354 var tfwPath = this.srcPath( "tfw" );
355 if ( !FS.existsSync( tfwPath ) ) {
356 Template.files( "tfw", tfwPath );
357 }
358 this.copyFile( tfwPath, this.wwwPath( 'tfw' ), false );
359};
360
361
362/**
363 * Copy resources in `css/`.
364 * @param {object} project - Current project.
365 * @param {array} sources - Array of compiled Html Files.
366 * @param {object} config - Current configuration from `package.json`.
367 * Array of objects of class `Source` representing each compiled HTML.
368 * @returns {undefined}
369 */
370function copyResources( project, sources, config ) {
371 const dependenciesForAllFiles = [];
372 if ( !Util.isEmpty( sources ) ) {
373 sources.forEach( function forEachSource( sourceHTML ) {
374 const output = sourceHTML.tag( "output" ) || {};
375 if ( !Array.isArray( output.modules ) ) {
376 return;
377 }
378 output.modules.forEach( function forEachModule( module ) {
379 if ( dependenciesForAllFiles.indexOf( module ) < 0 ) {
380 dependenciesForAllFiles.push( module );
381 }
382 } );
383 } );
384 }
385 if ( dependenciesForAllFiles.length > 0 ) {
386 console.log( "Copying resources...".cyan );
387 dependenciesForAllFiles.forEach( function forEachDependencyFile( module ) {
388 const src = project.srcOrLibPath( module );
389 if ( src ) {
390 const dst = project.wwwPath( Util.replaceFirstSubFolder( module, 'css/' ) );
391 project.copyFile( src, dst );
392 }
393 } );
394 }
395
396 // Extra resources defined in `package.json`.
397 const extraResources = config.tfw.resources || [];
398 extraResources.forEach( function forEachExtraResource( res ) {
399 console.log( `Extra resource: ${res.cyan}` );
400 project.copyFile(
401 project.srcPath( res ),
402 project.wwwPath( res )
403 );
404 } );
405}
406
407
408/**
409 * @return void
410 */
411Project.prototype.isReservedModules = function ( filename ) {
412 var reservedModules = this._config.reservedModules;
413 if ( !Array.isArray( reservedModules ) ) return false;
414 filename = filename.split( "/" ).pop();
415 if ( filename.substr( filename.length - 3 ) == '.js' ) {
416 // Remove extension.
417 filename = filename.substr( 0, filename.length - 3 );
418 }
419 if ( reservedModules.indexOf( filename ) > -1 ) return true;
420 return false;
421};
422
423/**
424 * @return module `Template`.
425 */
426Project.prototype.Template = Template;
427
428/**
429 * @return Tree module.
430 */
431Project.prototype.Tree = Tree;
432
433/**
434 * @param {string} path path relative to `lib/` in ToloFrameWork folder.
435 * @return an absolute path.
436 */
437Project.prototype.libPath = function ( path ) {
438 if ( path.substr( 0, this._srcDir.length ) == this._srcDir ) {
439 path = path.substr( this._srcDir.length );
440 }
441 return Path.resolve( Path.join( this._libDir, path ) );
442};
443
444/**
445 * @param {string} path path relative to `tpl/` in ToloFrameWork folder.
446 * @return an absolute path.
447 */
448Project.prototype.tplPath = function ( path ) {
449 if ( path.substr( 0, this._srcDir.length ) == this._srcDir ) {
450 path = path.substr( this._srcDir.length );
451 }
452 return Path.resolve( Path.join( this._tplDir, path ) );
453};
454
455/**
456 * @param {string} path - path relative to `src/`.
457 * @return {string} an absolute path.
458 */
459Project.prototype.srcPath = function ( path ) {
460 if ( typeof path === 'undefined' ) return this._srcDir;
461 if ( path.substr( 0, this._srcDir.length ) === this._srcDir ) {
462 path = path.substr( this._srcDir.length );
463 }
464 return Path.resolve( Path.join( this._srcDir, path ) );
465};
466
467/**
468 * @param {string} path path relative to `doc/`.
469 * @return an absolute path.
470 */
471Project.prototype.docPath = function ( path ) {
472 if ( typeof path === 'undefined' ) return this._docDir;
473 if ( path.substr( 0, this._srcDir.length ) == this._srcDir ) {
474 path = path.substr( this._srcDir.length );
475 }
476 return Path.resolve( Path.join( this._docDir, path ) );
477};
478
479/**
480 * @param {string} path path relative to the current page folder.
481 * @return an absolute path.
482 */
483Project.prototype.htmPath = function ( path ) {
484 if ( typeof path === 'undefined' ) return this._htmDir;
485 if ( path.substr( 0, this._srcDir.length ) == this._srcDir ) {
486 path = path.substr( this._srcDir.length );
487 }
488 return Path.resolve( Path.join( this._htmDir, path ) );
489};
490
491/**
492 * @param {string} path path relative to `prj/`.
493 * @return an absolute path.
494 */
495Project.prototype.prjPath = function ( path ) {
496 if ( typeof path === 'undefined' ) return this._prjDir;
497 if ( path.substr( 0, this._srcDir.length ) === this._srcDir ) {
498 path = path.substr( this._srcDir.length );
499 }
500 return Path.resolve( Path.join( this._prjDir, path ) );
501};
502
503/**
504 * @param {string} path path relative to `src/` or extenal modules or `lib/`.
505 * @return an absolute path or null if the file does not exist.
506 */
507Project.prototype.srcOrLibPath = function ( path ) {
508 if ( typeof path !== 'string' ) return path;
509
510 const
511 path2 = Util.replaceDotsWithSlashes( path ),
512 pathes = [ this.srcPath( path2 ), this.srcPath( path ) ];
513
514 this._modulesPath.forEach( function ( modulePath ) {
515 pathes.push(
516 Path.resolve( modulePath, path2 ),
517 Path.resolve( modulePath, path )
518 );
519 } );
520 pathes.push(
521 this.libPath( path2 ),
522 this.libPath( path )
523 );
524 for ( const file of pathes ) {
525 if ( FS.existsSync( file ) ) return file;
526 }
527 return null;
528};
529
530/**
531 * @return void
532 */
533Project.prototype.getExtraModulesPath = function () {
534 return this._modulesPath.slice();
535};
536
537
538/**
539 * @param {string} path path relative to `tmp/`.
540 * @return an absolute path.
541 */
542Project.prototype.tmpPath = function ( path ) {
543 if ( typeof path === 'undefined' ) return this._tmpDir;
544 if ( path.substr( 0, this._srcDir.length ) == this._srcDir ) {
545 path = path.substr( this._srcDir.length );
546 }
547 return Path.resolve( Path.join( this._tmpDir, path ) );
548};
549
550/**
551 * @param {string} path path relative to `www/`.
552 * @return an absolute path.
553 */
554Project.prototype.wwwPath = function ( path ) {
555 if ( typeof path === 'undefined' ) return this._wwwDir;
556 if ( path.substr( 0, this._srcDir.length ) == this._srcDir ) {
557 path = path.substr( this._srcDir.length );
558 }
559 return Path.resolve( Path.join( this._wwwDir, path ) );
560};
561
562/**
563 * @return Dictionary of available widget compilers. The key is the
564 * widget name, the value is an object:
565 * * __path__: absolute path of the compiler's' directory.
566 * * __name__: widget's name.
567 * * __compiler__: compiler's module owning functions such as `compile`, `precompile`, ...
568 * * __precompilation__: is this widget in mode _precompilation_? In this case, it must be called in the Top-Down walking.
569 */
570Project.prototype.getAvailableWidgetCompilers = function () {
571 if ( !this._availableWidgetsCompilers ) {
572 var map = {};
573 var dirs = [ this._srcDir, this._libDir ];
574 console.log( "Available widgets:" );
575 dirs.forEach(
576 // Resolve paths for "wdg/" directories.
577 function ( itm, idx, arr ) {
578 var path = Path.resolve( Path.join( itm, "wdg" ) );
579 if ( !FS.existsSync( path ) ) {
580 path = null;
581 }
582 arr[ idx ] = path;
583 }
584 );
585 dirs.forEach(
586 function ( dir, idx ) {
587 if ( typeof dir !== 'string' ) return;
588 var files = FS.readdirSync( dir );
589 files.forEach(
590 function ( filename ) {
591 var file = Path.join( dir, filename );
592 var stat = FS.statSync( file );
593 if ( stat.isFile() ) return;
594 if ( !map[ filename ] ) {
595 map[ filename ] = {
596 path: file,
597 name: filename
598 };
599 var modulePath = Path.join( file, "compile-" + filename + ".js" );
600 if ( FS.existsSync( modulePath ) ) {
601 var compiler = require( modulePath );
602 if ( typeof compiler.precompile === 'function' ) {
603 map[ filename ].precompilation = true;
604 map[ filename ].compiler = compiler;
605 } else if ( typeof compiler.compile === 'function' ) {
606 map[ filename ].compiler = compiler;
607 }
608 }
609 var name = ( filename.substr( 0, 1 ).toUpperCase() +
610 filename.substr( 1 ).toLowerCase() ).cyan;
611 if ( idx == 0 ) {
612 name = name.bold;
613 }
614 /*
615 console.log(
616 " " + ( map[ filename ].precompilation ? "<w:".yellow.bold : "<w:" ) +
617 name + ( map[ filename ].precompilation ? ">".yellow.bold : ">" ) +
618 "\t" + file
619 );
620 */
621 }
622 }
623 );
624 }
625 );
626 this._availableWidgetsCompilers = map;
627 }
628 return this._availableWidgetsCompilers;
629};
630
631/**
632 * Throw a fatal exception.
633 */
634Project.prototype.fatal = function ( msg, id, src ) {
635 Fatal.fire( msg, id, src );
636};
637
638/**
639 * Incrément version.
640 */
641Project.prototype.makeVersion = function ( options ) {
642 var cfg = this._config;
643 var version = cfg.version.split( "." );
644 if ( version.length < 3 ) {
645 while ( version.length < 3 ) version.push( "0" );
646 } else {
647 version[ version.length - 1 ] = ( parseInt( version[ version.length - 1 ] ) || 0 ) + 1;
648 }
649 cfg.version = version.join( "." );
650 PathUtils.file( this.prjPath( 'package.json' ), JSON.stringify( cfg, null, ' ' ) );
651 console.log( "New version: " + cfg.version.cyan );
652};
653
654
655/**
656 * Tests use __Karma__ and __Jasmine__ ans the root folder is `spec`.
657 * All the __tfw__ modules are put in `spec/mod` folder.
658 */
659Project.prototype.makeTest = function ( compiledFiles, specDir ) {
660 console.log( "Prepare Karma/Jasmine tests... ".cyan + "(" + specDir + ")" );
661
662 // Create missing folders.
663 var specPath = this.prjPath( specDir );
664 if ( !FS.existsSync( specPath ) ) {
665 FS.mkdir( specPath );
666 }
667 if ( !FS.existsSync( Path.join( specPath, "mod" ) ) ) {
668 FS.mkdir( Path.join( specPath, "mod" ) );
669 }
670
671 // List of needed modules.
672 var allModules = [];
673 compiledFiles.forEach( function ( compiledFile ) {
674 var output = compiledFile.tag( 'output' );
675 var modules = output.modules;
676 modules.forEach( function ( module ) {
677 if ( allModules.indexOf( allModules ) < 0 ) {
678 allModules.push( module );
679 }
680 } );
681 } );
682
683 // Copy modules in `spec/mod`.
684 allModules.forEach( function ( module ) {
685 var js = new Source( this, module + ".js" );
686 PathUtils.file( this.prjPath( specDir + "/" + module + ".js" ), js.tag( 'zip' ) );
687 }, this );
688
689 PathUtils.file( this.prjPath( specDir + "/mod/@require.js" ), Template.file( 'require.js' ).out );
690};
691
692
693/**
694 * Writing documentation.
695 * @return void
696 */
697Project.prototype.makeDoc = function () {
698 console.log( "Writing documentation...".cyan );
699 CompilerPHP.compile( this );
700 var that = this;
701 var modules = "window.M={";
702 this._modulesList.sort();
703 this._modulesList.forEach(
704 function ( moduleName, index ) {
705 var src = new Source( that, "mod/" + moduleName );
706 if ( index > 0 ) modules += ",\n";
707 modules += JSON.stringify( moduleName.substr( 0, moduleName.length - 3 ) ) + ":" +
708 JSON.stringify( src.tag( "doc" ) );
709 }
710 );
711 modules += "}";
712 var cfg = this._config;
713 var docPath = this.prjPath( "doc" );
714 Template.files( "doc", docPath, {
715 project: cfg.name
716 } );
717 this.mkdir( docPath );
718 PathUtils.file(
719 Path.join( docPath, "modules.js" ),
720 modules
721 );
722};
723
724Project.prototype.makeJSDoc = function () {
725 console.error( "Not implemented yet!" );
726};
727
728/**
729 * @return {this}
730 */
731Project.prototype.addModuleToList = function ( moduleName ) {
732 if ( moduleName.substr( 0, 4 ) != 'mod/' ) return this;
733 moduleName = moduleName.substr( 4 );
734 if ( moduleName.charAt( 0 ) == '$' ) return this;
735 if ( this._modulesList.indexOf( moduleName ) < 0 ) {
736 this._modulesList.push( moduleName );
737 }
738 return this;
739};
740
741/**
742 * Link every `*.html` file found in _srcDir_.
743 */
744Project.prototype.link = function () {
745 console.log( "Cleaning output: " + this.wwwPath( 'js' ) );
746 Util.cleanDir( this.wwwPath( 'js' ) );
747 console.log( "Cleaning output: " + this.wwwPath( 'css' ) );
748 Util.cleanDir( this.wwwPath( 'css' ) );
749 this.mkdir( this.wwwPath( "DEBUG" ) );
750 this.mkdir( this.wwwPath( "RELEASE" ) );
751 this._htmlFiles.forEach(
752 function ( filename ) {
753 filename = filename.split( Path.sep ).join( "/" );
754 console.log( "Linking " + filename.yellow.bold );
755 var shiftPath = "";
756 var subdirCount = filename.split( "/" ).length - 1;
757 for ( var i = 0; i < subdirCount; i++ ) {
758 shiftPath += "../";
759 }
760 this.linkForDebug( filename, shiftPath );
761 this.linkForRelease( filename, shiftPath );
762 },
763 this
764 );
765};
766
767/**
768 * @return void
769 */
770Project.prototype.sortCSS = function ( linkJS, linkCSS ) {
771 var input = [];
772 linkCSS.forEach(
773 function ( nameCSS, indexCSS ) {
774 var nameJS = nameCSS.substr( 0, nameCSS.length - 3 ) + "js";
775 var pos = linkJS.indexOf( nameJS );
776 if ( pos < 0 ) pos = 1000000 + indexCSS;
777 input.push( [ nameCSS, pos ] );
778 }
779 );
780 input.sort(
781 function ( a, b ) {
782 var x = a[ 0 ];
783 var y = b[ 0 ];
784 if ( x < y ) return -1;
785 if ( x > y ) return 1;
786 x = a[ 1 ];
787 y = b[ 1 ];
788 if ( x < y ) return -1;
789 if ( x > y ) return 1;
790 return 0;
791 }
792 );
793 return input.map( function ( x ) {
794 return x[ 0 ];
795 } );
796};
797
798Project.prototype.sortJS = function ( srcHTML, linkJS ) {
799 var input = [];
800 linkJS.forEach(
801 function ( nameJS ) {
802 var srcJS = srcHTML.create( nameJS );
803 var item = {
804 key: nameJS,
805 dep: []
806 };
807 srcJS.tag( "needs" ).forEach(
808 function ( name ) {
809 if ( name != nameJS && linkJS.indexOf( name ) > -1 ) {
810 item.dep.push( name );
811 }
812 }
813 );
814 input.push( item );
815 }
816 );
817 return this.topologicalSort( input );
818};
819
820Project.prototype.topologicalSort = function ( input ) {
821 var output = [];
822 while ( output.length < input.length ) {
823 // Looking for the less depending item.
824 var candidate = null;
825 input.forEach(
826 function ( item ) {
827 if ( !item.key ) return;
828 if ( !candidate ) {
829 candidate = item;
830 } else {
831 if ( item.dep.length < candidate.dep.length ) {
832 candidate = item;
833 }
834 }
835 }
836 );
837 // This candidate is the next item of the output list.
838 var key = candidate.key;
839 output.push( key );
840 delete candidate.key;
841 // Remove this item in all the dependency lists.
842 input.forEach(
843 function ( item ) {
844 if ( !item.key ) return;
845 item.dep = item.dep.filter(
846 function ( x ) {
847 return x != key;
848 }
849 );
850 }
851 );
852 }
853 return output;
854};
855
856/**
857 * Linking in DEBUG mode.
858 * Starting with an HTML file, we will find all dependent JS and CSS.
859 *
860 * Example: filename = "foo/bar.html"
861 * We will create:
862 * * `DEBUG/js/foo/@bar.js` for inner JS.
863 * * `DEBUG/css/foo/@bar.css` for inner CSS.
864 */
865Project.prototype.linkForDebug = function ( filename, shiftPath ) {
866 // Add this to a Javascript link to force webserver to deliver a non cached file.
867 var seed = "?" + Date.now();
868 // The HTML source file.
869 var srcHTML = new Source( this, filename );
870 // Array of all needed JS topologically sorted.
871 var linkJS = this.sortJS( srcHTML, srcHTML.tag( "linkJS" ) || [] );
872 // Array of all needed CSS topologically sorted.
873 var linkCSS = this.sortCSS( linkJS, srcHTML.tag( "linkCSS" ) || [] );
874 // HTML tree structure.
875 var tree = Tree.clone( srcHTML.tag( "tree" ) );
876 var manifestFiles = [];
877
878 var head = Tree.getElementByName( tree, "head" );
879 if ( !head ) {
880 this.fatal(
881 "Invalid HTML file: missing <head></head>!" +
882 "\n\n" +
883 Tree.toString( tree )
884 );
885 }
886
887 // Needed CSS files.
888 var cssDir = this.mkdir( this.wwwPath( "DEBUG/css" ) );
889 linkCSS.forEach(
890 function ( item ) {
891 var srcCSS = srcHTML.create( item );
892 var shortName = Path.basename( srcCSS.getAbsoluteFilePath() );
893 var output = Path.join( cssDir, shortName );
894 PathUtils.file( output, srcCSS.tag( "debug" ) );
895 if ( !head.children ) head.children = [];
896 head.children.push(
897 Tree.tag(
898 "link", {
899 href: shiftPath + "css/" + shortName + seed,
900 rel: "stylesheet",
901 type: "text/css"
902 }
903 )
904 );
905 head.children.push( {
906 type: Tree.TEXT,
907 text: "\n"
908 } );
909 manifestFiles.push( "css/" + shortName );
910 var resources = srcCSS.listResources();
911 resources.forEach(
912 function ( resource ) {
913 var shortName = "css/" + resource[ 0 ];
914 var longName = resource[ 1 ];
915 manifestFiles.push( shortName );
916 this.copyFile( longName, Path.join( this.wwwPath( "DEBUG" ), shortName ) );
917 },
918 this
919 );
920 },
921 this
922 );
923
924 // For type "nodewebkit", all JS must lie in "node_modules" and they
925 // don't need to be declared in the HTML file.
926 var jsDirShortName = ( this._type == 'nodewebkit' ? "node_modules" : "js" );
927 var jsDir = this.mkdir( this.wwwPath( "DEBUG/" + jsDirShortName ) );
928 linkJS.forEach(
929 function ( item ) {
930 var srcJS = srcHTML.create( item );
931 var shortName = Path.basename( srcJS.getAbsoluteFilePath() );
932 var output = Path.join( jsDir, shortName );
933 var code = srcJS.read();
934 if ( item.substr( 0, 4 ) == 'mod/' ) {
935 if ( this._type == 'nodewebkit' ) {
936 // Let's add internationalisation snippet.
937 code = ( srcJS.tag( "intl" ) || "" ) + code;
938 } else {
939 // This is a module. We need to wrap it in module's declaration snippet.
940 code =
941 "require('" +
942 shortName.substr( 0, shortName.length - 3 ).toLowerCase() +
943 "', function(exports, module){\n" +
944 ( srcJS.tag( "intl" ) || "" ) +
945 code +
946 "\n});\n";
947 }
948 }
949 PathUtils.file( output, code );
950 if ( this._type != 'nodewebkit' ) {
951 // Declaration and manifest only needed for project of
952 // type that is not "nodewebkit".
953 if ( !head.children ) head.children = [];
954 head.children.push(
955 Tree.tag(
956 "script", {
957 src: shiftPath + jsDirShortName + "/" + shortName + seed
958 }
959 )
960 );
961 head.children.push( {
962 type: Tree.TEXT,
963 text: "\n"
964 } );
965 manifestFiles.push( jsDirShortName + "/" + shortName );
966 }
967 },
968 this
969 );
970 srcHTML.tag( "resources" ).forEach(
971 function ( itm, idx, arr ) {
972 var src = itm;
973 var dst = src;
974 if ( Array.isArray( src ) ) {
975 dst = src[ 1 ];
976 src = src[ 0 ];
977 }
978 manifestFiles.push( dst );
979 src = this.srcPath( src );
980 dst = Path.join( this.wwwPath( "DEBUG" ), dst );
981 this.copyFile( src, dst );
982 }, this
983 );
984
985 // Adding innerJS and innerCSS.
986 var shortNameJS = PathUtils.addPrefix( filename.substr( 0, filename.length - 5 ), "@" ) + ".js";
987 head.children.push(
988 Tree.tag(
989 "script", {
990 src: shiftPath + jsDirShortName + "/" + shortNameJS + seed
991 }
992 )
993 );
994 manifestFiles.push( jsDirShortName + "/" + shortNameJS );
995 var wwwInnerJS = Path.join( jsDir, shortNameJS );
996 PathUtils.file(
997 wwwInnerJS,
998 srcHTML.tag( "innerJS" )
999 );
1000
1001 if ( true ) {
1002 // For now, we decided to put the CSS relative to the inner HTML into the <head>'s tag.
1003 head.children.push(
1004 Tree.tag( "style", {}, srcHTML.tag( "innerCSS" ) )
1005 );
1006 } else {
1007 // If we want to externalise the inner CSS in the future, we can use this piece of code.
1008 var shortNameCSS = PathUtils.addPrefix( filename.substr( 0, filename.length - 5 ), "@" ) + ".css";
1009 head.children.push(
1010 Tree.tag(
1011 "link", {
1012 href: shiftPath + "css/" + shortNameCSS + seed,
1013 rel: "stylesheet",
1014 type: "text/css"
1015 }
1016 )
1017 );
1018 manifestFiles.push( shiftPath + "css/" + shortNameCSS );
1019 PathUtils.file(
1020 Path.join( cssDir, shortNameCSS ),
1021 srcHTML.tag( "innerCSS" )
1022 );
1023 }
1024
1025 if ( this._type != 'nodewebkit' ) {
1026 // Looking for manifest file.
1027 var html = Tree.findChild( tree, "html" );
1028 if ( html ) {
1029 var manifestFilename = Tree.att( "manifest" );
1030 if ( manifestFilename ) {
1031 // Writing manifest file only if needed.
1032 PathUtils.file(
1033 Path.join( this.wwwPath( "DEBUG" ), filename + ".manifest" ),
1034 "CACHE MANIFEST\n" +
1035 "# " + ( new Date() ) + " - " + Date.now() + "\n\n" +
1036 "CACHE:\n" +
1037 manifestFiles.join( "\n" ) +
1038 "\n\nNETWORK:\n*\n"
1039 );
1040 }
1041 }
1042 }
1043 // Writing HTML file.
1044 PathUtils.file(
1045 Path.join( this.wwwPath( "DEBUG" ), filename ),
1046 "<!-- " + ( new Date() ).toString() + " -->" +
1047 "<!DOCTYPE html>" + Tree.toString( tree )
1048 );
1049 // Writing ".htaccess" file.
1050 this.writeHtaccess( "DEBUG" );
1051 // Looking for webapp manifest for Firefox OS (also used for nodewebkit but with another name).
1052 copyManifestWebapp.call( this, "DEBUG" );
1053};
1054
1055/**
1056 * @param mode can be "RELEASE" or "DEBUG".
1057 * @return void
1058 */
1059Project.prototype.writeHtaccess = function ( mode ) {
1060 PathUtils.file(
1061 Path.join( this.wwwPath( mode ), ".htaccess" ),
1062 "AddType application/x-web-app-manifest+json .webapp\n" +
1063 "AddType text/cache-manifest .manifest\n" +
1064 "ExpiresByType text/cache-manifest \"access plus 0 seconds\"\n" +
1065 "Header set Expires \"Thu, 19 Nov 1981 08:52:00 GM\"\n" +
1066 "Header set Cache-Control \"no-store, no-cache, must-revalidate, post-check=0, pre-check=0\"\n" +
1067 "Header set Pragma \"no-cache\"\n"
1068 );
1069};
1070
1071/**
1072 * @param mode : "DEBUG" or "RELEASE".
1073 */
1074function copyManifestWebapp( mode ) {
1075 var filename = "manifest.webapp";
1076 var out;
1077 if ( this._type == 'nodewebkit' ) filename = "package.json";
1078
1079 console.log( "Copying " + filename.cyan + "..." );
1080
1081 // Looking for webapp manifest for Firefox OS.
1082 if ( false == FS.existsSync( this.srcPath( filename ) ) ) {
1083 out = Template.file( filename, this._config ).out;
1084 PathUtils.file( this.srcPath( filename ), out );
1085 }
1086 var webappFile = this.srcPath( filename );
1087 if ( webappFile ) {
1088 var content = FS.readFileSync( webappFile ).toString();
1089 var json = null;
1090 try {
1091 json = JSON.parse( content );
1092 } catch ( x ) {
1093 this.fatal( "'" + filename + "' must be a valid JSON file!\n" + x );
1094 }
1095 json.version = this._config.version;
1096 if ( typeof json.window === 'object' ) {
1097 json.window.toolbar = ( mode == "DEBUG" );
1098 }
1099 PathUtils.file( Path.join( this.wwwPath( mode ), filename ), JSON.stringify( json, null, 4 ) );
1100 var icons = json.icons || {};
1101 var key, val;
1102 for ( key in icons ) {
1103 val = this.srcOrLibPath( icons[ key ] );
1104 if ( val ) {
1105 this.copyFile( val, Path.join( this.wwwPath( mode ), icons[ key ] ) );
1106 }
1107 }
1108 }
1109}
1110
1111/**
1112 * @return array of HTML files found in _srcDir_.
1113 */
1114Project.prototype.findHtmlFiles = function () {
1115 var that = this;
1116
1117 var filters = this._config.tfw.compile.files;
1118 if ( typeof filters === 'undefined' ) filters = "\\.html$";
1119 var files = [],
1120 srcDir = this.srcPath(),
1121 prefixLength = srcDir.length + 1,
1122 filter, i, rxFilters = [],
1123 arr;
1124 if ( !Array.isArray( filters ) ) {
1125 filters = [ filters ];
1126 }
1127 for ( i = 0; i < filters.length; i++ ) {
1128 filter = filters[ i ];
1129 if ( typeof filter !== 'string' ) {
1130 this.fatal( "Invalid atribute \"tfw.compile.files\" in \"package.json\"!\n" +
1131 "Must be a string or an array of strings." );
1132 }
1133 arr = [];
1134 filter.split( "/" ).forEach(
1135 function ( item ) {
1136 try {
1137 item = item.trim();
1138 if ( item == '' || item == '*' ) {
1139 // `null`matches anything.
1140 arr.push( null );
1141 } else {
1142 arr.push( new RegExp( item, "i" ) );
1143 }
1144 } catch ( ex ) {
1145 this.fatal(
1146 "Invalid regular expression for filter: " + JSON.stringify( filter ) + "!"
1147 );
1148 }
1149 }
1150 );
1151 rxFilters.push( arr );
1152 }
1153 rxFilters.forEach(
1154 function ( f ) {
1155 PathUtils.findFiles( srcDir, f ).forEach(
1156 function ( item ) {
1157 if ( item.substr( item.length - 5 ).toLowerCase() !== '.html' ) {
1158 console.log( "Copying " + item.yellow );
1159 that.copyFile( item, that.wwwPath( Path.basename( item ) ) );
1160 return;
1161 }
1162 files.push( item.substr( prefixLength ) );
1163 }
1164 );
1165 }
1166 );
1167 if ( files.length == 0 ) {
1168 this.fatal(
1169 "No HTML file found!\n\nPattern: " + JSON.stringify( filters ) +
1170 "\nFolder: " + srcDir
1171 );
1172 }
1173 return files;
1174};
1175
1176/**
1177 * @param arguments all arguments will be joined to form the path of the directory to create.
1178 * @return the name of the created directory.
1179 */
1180Project.prototype.mkdir = function () {
1181 var key, arg, items = [];
1182 for ( key in arguments ) {
1183 arg = arguments[ key ].trim();
1184 items.push( arg );
1185 }
1186 var path = Path.resolve( Path.normalize( items.join( "/" ) ) ),
1187 item, i,
1188 curPath = "";
1189 items = path.replace( /\\/g, '/' ).split( "/" );
1190 for ( i = 0; i < items.length; i++ ) {
1191 item = items[ i ];
1192 curPath += item + "/";
1193 if ( FS.existsSync( curPath ) ) {
1194 var stat = FS.statSync( curPath );
1195 if ( !stat.isDirectory() ) {
1196 break;
1197 }
1198 } else {
1199 try {
1200 FS.mkdirSync( curPath );
1201 } catch ( ex ) {
1202 throw {
1203 fatal: "Unable to create directory \"" + curPath + "\"!\n" + ex
1204 };
1205 }
1206 }
1207 }
1208 return path;
1209};
1210
1211// Used for file copy.
1212var buffer = new Buffer( 64 * 1024 );
1213
1214/**
1215 * Copy a file from `src` to `dst`.
1216 * @param src full path of the source file.
1217 * @param dst full path of the destination file.
1218 */
1219Project.prototype.copyFile = function ( src, dst, log ) {
1220 if ( log ) {
1221 console.log( "copyFile( " + src.cyan.bold + ", " + dst + " )" );
1222 }
1223 if ( !FS.existsSync( src ) ) {
1224 this.fatal( "Unable to copy missing file: " + src + "\ninto: " + dst, -1, src );
1225 }
1226 var stat = FS.statSync( src );
1227 if ( stat.isDirectory() ) {
1228 // We need to copy a whole directory.
1229 if ( FS.existsSync( dst ) ) {
1230 // Check if the destination is a directory.
1231 stat = FS.statSync( dst );
1232 if ( !stat.isDirectory() ) {
1233 this.fatal( "Destination is not a directory: \"" + dst +
1234 "\"!\nSource is \"" + src + "\".", -1, "project.copyFile" );
1235 }
1236 } else {
1237 // Make destination directory.
1238 this.mkdir( dst );
1239 }
1240 var files = FS.readdirSync( src );
1241 files.forEach(
1242 function ( filename ) {
1243 this.copyFile(
1244 Path.join( src, filename ),
1245 Path.join( dst, filename ),
1246 log
1247 );
1248 },
1249 this
1250 );
1251 return;
1252 }
1253
1254 var bytesRead, pos, rfd, wfd;
1255 this.mkdir( Path.dirname( dst ) );
1256 try {
1257 rfd = FS.openSync( src, "r" );
1258 } catch ( ex ) {
1259 this.fatal( "Unable to open file \"" + src + "\" for reading!\n" + ex, -1, "project.copyFile" );
1260 }
1261 try {
1262 wfd = FS.openSync( dst, "w" );
1263 } catch ( ex ) {
1264 this.fatal( "Unable to open file \"" + dst + "\" for writing!\n" + ex, -1, "project.copyFile" );
1265 }
1266 bytesRead = 1;
1267 pos = 0;
1268 while ( bytesRead > 0 ) {
1269 try {
1270 bytesRead = FS.readSync( rfd, buffer, 0, 64 * 1024, pos );
1271 } catch ( ex ) {
1272 this.fatal( "Unable to read file \"" + src + "\"!\n" + ex, -1, "project.copyFile" );
1273 }
1274 FS.writeSync( wfd, buffer, 0, bytesRead );
1275 pos += bytesRead;
1276 }
1277 FS.closeSync( rfd );
1278 return FS.closeSync( wfd );
1279};
1280
1281
1282/**
1283 * @param {string} prjDir root directory of the project. It is where we can find `project.tfw.json`.
1284 * @return {object} instance of the class `Project`.
1285 */
1286function createProject( prjDir ) {
1287 return new Project( prjDir );
1288}
1289
1290
1291/**
1292 * @param {object} project - Instance of Project.
1293 * @returns {undefined}
1294 */
1295function initializedPrivateVariables( project ) {
1296 // Will own the list of HTML files as Source objects.
1297 project._compiledFiles = [];
1298
1299 /*
1300 * To prevent double flushing of the same file, this array keeps the
1301 * name of the already flushed files.
1302 * @see `this.flushContent()`
1303 */
1304 project._flushedContent = [];
1305
1306 project._modulesPath = [];
1307}
1308
1309
1310/**
1311 * @param {string} version - Version in format `major.minor.patch`.
1312 * @returns {ojbect} `{major, minor, patch}`.
1313 */
1314function parseVersion( version ) {
1315 try {
1316 const
1317 versionArray = version.split( "." ),
1318 MAJOR_INDEX = 0,
1319 MINOR_INDEX = 1,
1320 PATCH_INDEX = 2;
1321 return {
1322 major: versionArray[ MAJOR_INDEX ],
1323 minor: versionArray[ MINOR_INDEX ],
1324 patch: versionArray[ PATCH_INDEX ]
1325 };
1326 } catch ( ex ) {
1327 throw new Error( `Unable to parse version ${JSON.stringify(version, null, ' ')}` );
1328 }
1329}
1330
1331/**
1332 * @param {object} cfg - Configuration.
1333 * @returns {object} The minimal object that we must provide as the module `$.js`.
1334 */
1335function getModuleContentFor$( cfg ) {
1336 const version = parseVersion( cfg.version );
1337 return {
1338 name: JSON.stringify( cfg.name ),
1339 description: JSON.stringify( cfg.description || "" ),
1340 author: JSON.stringify( cfg.author || "" ),
1341 version: JSON.stringify( cfg.version ),
1342 major: version.major,
1343 minor: version.minor,
1344 revision: version.patch,
1345 date: new Date(),
1346 consts: {}
1347 };
1348}
1349
1350
1351/**
1352 * @param {array} htmlFiles - List of all the HTML files we add to compile.
1353 * @param {object} options - Options for debug, release, ...
1354 * @returns {array} List of actually compiled files.
1355 * @see findHtmlFiles
1356 */
1357function compileHtmlFiles( htmlFiles, options ) {
1358 const compiledFiles = [];
1359 for ( let i = 0; i < htmlFiles.length; i++ ) {
1360 const filename = htmlFiles[ i ];
1361 try {
1362 const compiledFile = CompilerHTML2.compile( filename, Util.clone( options ) );
1363 compiledFiles.push( compiledFile );
1364 } catch ( ex ) {
1365 Fatal.bubble( ex, filename );
1366 }
1367 }
1368 return compiledFiles;
1369}
\No newline at end of file