UNPKG

45.2 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 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 var result = this.srcPath( path );
509 if ( FS.existsSync( result ) ) return result;
510 for ( var i = 0; i < this._modulesPath.length; i++ ) {
511 result = this._modulesPath[ i ];
512 result = Path.resolve( result, path );
513 if ( FS.existsSync( result ) ) return result;
514 }
515 result = this.libPath( path );
516 if ( FS.existsSync( result ) ) return result;
517 return null;
518};
519
520/**
521 * @return void
522 */
523Project.prototype.getExtraModulesPath = function () {
524 return this._modulesPath.slice();
525};
526
527
528/**
529 * @param {string} path path relative to `tmp/`.
530 * @return an absolute path.
531 */
532Project.prototype.tmpPath = function ( path ) {
533 if ( typeof path === 'undefined' ) return this._tmpDir;
534 if ( path.substr( 0, this._srcDir.length ) == this._srcDir ) {
535 path = path.substr( this._srcDir.length );
536 }
537 return Path.resolve( Path.join( this._tmpDir, path ) );
538};
539
540/**
541 * @param {string} path path relative to `www/`.
542 * @return an absolute path.
543 */
544Project.prototype.wwwPath = function ( path ) {
545 if ( typeof path === 'undefined' ) return this._wwwDir;
546 if ( path.substr( 0, this._srcDir.length ) == this._srcDir ) {
547 path = path.substr( this._srcDir.length );
548 }
549 return Path.resolve( Path.join( this._wwwDir, path ) );
550};
551
552/**
553 * @return Dictionary of available widget compilers. The key is the
554 * widget name, the value is an object:
555 * * __path__: absolute path of the compiler's' directory.
556 * * __name__: widget's name.
557 * * __compiler__: compiler's module owning functions such as `compile`, `precompile`, ...
558 * * __precompilation__: is this widget in mode _precompilation_? In this case, it must be called in the Top-Down walking.
559 */
560Project.prototype.getAvailableWidgetCompilers = function () {
561 if ( !this._availableWidgetsCompilers ) {
562 var map = {};
563 var dirs = [ this._srcDir, this._libDir ];
564 console.log( "Available widgets:" );
565 dirs.forEach(
566 // Resolve paths for "wdg/" directories.
567 function ( itm, idx, arr ) {
568 var path = Path.resolve( Path.join( itm, "wdg" ) );
569 if ( !FS.existsSync( path ) ) {
570 path = null;
571 }
572 arr[ idx ] = path;
573 }
574 );
575 dirs.forEach(
576 function ( dir, idx ) {
577 if ( typeof dir !== 'string' ) return;
578 var files = FS.readdirSync( dir );
579 files.forEach(
580 function ( filename ) {
581 var file = Path.join( dir, filename );
582 var stat = FS.statSync( file );
583 if ( stat.isFile() ) return;
584 if ( !map[ filename ] ) {
585 map[ filename ] = {
586 path: file,
587 name: filename
588 };
589 var modulePath = Path.join( file, "compile-" + filename + ".js" );
590 if ( FS.existsSync( modulePath ) ) {
591 var compiler = require( modulePath );
592 if ( typeof compiler.precompile === 'function' ) {
593 map[ filename ].precompilation = true;
594 map[ filename ].compiler = compiler;
595 } else if ( typeof compiler.compile === 'function' ) {
596 map[ filename ].compiler = compiler;
597 }
598 }
599 var name = ( filename.substr( 0, 1 ).toUpperCase() +
600 filename.substr( 1 ).toLowerCase() ).cyan;
601 if ( idx == 0 ) {
602 name = name.bold;
603 }
604 /*
605 console.log(
606 " " + ( map[ filename ].precompilation ? "<w:".yellow.bold : "<w:" ) +
607 name + ( map[ filename ].precompilation ? ">".yellow.bold : ">" ) +
608 "\t" + file
609 );
610 */
611 }
612 }
613 );
614 }
615 );
616 this._availableWidgetsCompilers = map;
617 }
618 return this._availableWidgetsCompilers;
619};
620
621/**
622 * Throw a fatal exception.
623 */
624Project.prototype.fatal = function ( msg, id, src ) {
625 Fatal.fire( msg, id, src );
626};
627
628/**
629 * Incrément version.
630 */
631Project.prototype.makeVersion = function ( options ) {
632 var cfg = this._config;
633 var version = cfg.version.split( "." );
634 if ( version.length < 3 ) {
635 while ( version.length < 3 ) version.push( "0" );
636 } else {
637 version[ version.length - 1 ] = ( parseInt( version[ version.length - 1 ] ) || 0 ) + 1;
638 }
639 cfg.version = version.join( "." );
640 PathUtils.file( this.prjPath( 'package.json' ), JSON.stringify( cfg, null, ' ' ) );
641 console.log( "New version: " + cfg.version.cyan );
642};
643
644
645/**
646 * Tests use __Karma__ and __Jasmine__ ans the root folder is `spec`.
647 * All the __tfw__ modules are put in `spec/mod` folder.
648 */
649Project.prototype.makeTest = function ( compiledFiles, specDir ) {
650 console.log( "Prepare Karma/Jasmine tests... ".cyan + "(" + specDir + ")" );
651
652 // Create missing folders.
653 var specPath = this.prjPath( specDir );
654 if ( !FS.existsSync( specPath ) ) {
655 FS.mkdir( specPath );
656 }
657 if ( !FS.existsSync( Path.join( specPath, "mod" ) ) ) {
658 FS.mkdir( Path.join( specPath, "mod" ) );
659 }
660
661 // List of needed modules.
662 var allModules = [];
663 compiledFiles.forEach( function ( compiledFile ) {
664 var output = compiledFile.tag( 'output' );
665 var modules = output.modules;
666 modules.forEach( function ( module ) {
667 if ( allModules.indexOf( allModules ) < 0 ) {
668 allModules.push( module );
669 }
670 } );
671 } );
672
673 // Copy modules in `spec/mod`.
674 allModules.forEach( function ( module ) {
675 var js = new Source( this, module + ".js" );
676 PathUtils.file( this.prjPath( specDir + "/" + module + ".js" ), js.tag( 'zip' ) );
677 }, this );
678
679 PathUtils.file( this.prjPath( specDir + "/mod/@require.js" ), Template.file( 'require.js' ).out );
680};
681
682
683/**
684 * Writing documentation.
685 * @return void
686 */
687Project.prototype.makeDoc = function () {
688 console.log( "Writing documentation...".cyan );
689 CompilerPHP.compile( this );
690 var that = this;
691 var modules = "window.M={";
692 this._modulesList.sort();
693 this._modulesList.forEach(
694 function ( moduleName, index ) {
695 var src = new Source( that, "mod/" + moduleName );
696 if ( index > 0 ) modules += ",\n";
697 modules += JSON.stringify( moduleName.substr( 0, moduleName.length - 3 ) ) + ":" +
698 JSON.stringify( src.tag( "doc" ) );
699 }
700 );
701 modules += "}";
702 var cfg = this._config;
703 var docPath = this.prjPath( "doc" );
704 Template.files( "doc", docPath, {
705 project: cfg.name
706 } );
707 this.mkdir( docPath );
708 PathUtils.file(
709 Path.join( docPath, "modules.js" ),
710 modules
711 );
712};
713
714Project.prototype.makeJSDoc = function () {
715 console.error( "Not implemented yet!" );
716};
717
718/**
719 * @return {this}
720 */
721Project.prototype.addModuleToList = function ( moduleName ) {
722 if ( moduleName.substr( 0, 4 ) != 'mod/' ) return this;
723 moduleName = moduleName.substr( 4 );
724 if ( moduleName.charAt( 0 ) == '$' ) return this;
725 if ( this._modulesList.indexOf( moduleName ) < 0 ) {
726 this._modulesList.push( moduleName );
727 }
728 return this;
729};
730
731/**
732 * Link every `*.html` file found in _srcDir_.
733 */
734Project.prototype.link = function () {
735 console.log( "Cleaning output: " + this.wwwPath( 'js' ) );
736 Util.cleanDir( this.wwwPath( 'js' ) );
737 console.log( "Cleaning output: " + this.wwwPath( 'css' ) );
738 Util.cleanDir( this.wwwPath( 'css' ) );
739 this.mkdir( this.wwwPath( "DEBUG" ) );
740 this.mkdir( this.wwwPath( "RELEASE" ) );
741 this._htmlFiles.forEach(
742 function ( filename ) {
743 filename = filename.split( Path.sep ).join( "/" );
744 console.log( "Linking " + filename.yellow.bold );
745 var shiftPath = "";
746 var subdirCount = filename.split( "/" ).length - 1;
747 for ( var i = 0; i < subdirCount; i++ ) {
748 shiftPath += "../";
749 }
750 this.linkForDebug( filename, shiftPath );
751 this.linkForRelease( filename, shiftPath );
752 },
753 this
754 );
755};
756
757/**
758 * @return void
759 */
760Project.prototype.sortCSS = function ( linkJS, linkCSS ) {
761 var input = [];
762 linkCSS.forEach(
763 function ( nameCSS, indexCSS ) {
764 var nameJS = nameCSS.substr( 0, nameCSS.length - 3 ) + "js";
765 var pos = linkJS.indexOf( nameJS );
766 if ( pos < 0 ) pos = 1000000 + indexCSS;
767 input.push( [ nameCSS, pos ] );
768 }
769 );
770 input.sort(
771 function ( a, b ) {
772 var x = a[ 0 ];
773 var y = b[ 0 ];
774 if ( x < y ) return -1;
775 if ( x > y ) return 1;
776 x = a[ 1 ];
777 y = b[ 1 ];
778 if ( x < y ) return -1;
779 if ( x > y ) return 1;
780 return 0;
781 }
782 );
783 return input.map( function ( x ) {
784 return x[ 0 ];
785 } );
786};
787
788Project.prototype.sortJS = function ( srcHTML, linkJS ) {
789 var input = [];
790 linkJS.forEach(
791 function ( nameJS ) {
792 var srcJS = srcHTML.create( nameJS );
793 var item = {
794 key: nameJS,
795 dep: []
796 };
797 srcJS.tag( "needs" ).forEach(
798 function ( name ) {
799 if ( name != nameJS && linkJS.indexOf( name ) > -1 ) {
800 item.dep.push( name );
801 }
802 }
803 );
804 input.push( item );
805 }
806 );
807 return this.topologicalSort( input );
808};
809
810Project.prototype.topologicalSort = function ( input ) {
811 var output = [];
812 while ( output.length < input.length ) {
813 // Looking for the less depending item.
814 var candidate = null;
815 input.forEach(
816 function ( item ) {
817 if ( !item.key ) return;
818 if ( !candidate ) {
819 candidate = item;
820 } else {
821 if ( item.dep.length < candidate.dep.length ) {
822 candidate = item;
823 }
824 }
825 }
826 );
827 // This candidate is the next item of the output list.
828 var key = candidate.key;
829 output.push( key );
830 delete candidate.key;
831 // Remove this item in all the dependency lists.
832 input.forEach(
833 function ( item ) {
834 if ( !item.key ) return;
835 item.dep = item.dep.filter(
836 function ( x ) {
837 return x != key;
838 }
839 );
840 }
841 );
842 }
843 return output;
844};
845
846/**
847 * Linking in DEBUG mode.
848 * Starting with an HTML file, we will find all dependent JS and CSS.
849 *
850 * Example: filename = "foo/bar.html"
851 * We will create:
852 * * `DEBUG/js/foo/@bar.js` for inner JS.
853 * * `DEBUG/css/foo/@bar.css` for inner CSS.
854 */
855Project.prototype.linkForDebug = function ( filename, shiftPath ) {
856 // Add this to a Javascript link to force webserver to deliver a non cached file.
857 var seed = "?" + Date.now();
858 // The HTML source file.
859 var srcHTML = new Source( this, filename );
860 // Array of all needed JS topologically sorted.
861 var linkJS = this.sortJS( srcHTML, srcHTML.tag( "linkJS" ) || [] );
862 // Array of all needed CSS topologically sorted.
863 var linkCSS = this.sortCSS( linkJS, srcHTML.tag( "linkCSS" ) || [] );
864 // HTML tree structure.
865 var tree = Tree.clone( srcHTML.tag( "tree" ) );
866 var manifestFiles = [];
867
868 var head = Tree.getElementByName( tree, "head" );
869 if ( !head ) {
870 this.fatal(
871 "Invalid HTML file: missing <head></head>!" +
872 "\n\n" +
873 Tree.toString( tree )
874 );
875 }
876
877 // Needed CSS files.
878 var cssDir = this.mkdir( this.wwwPath( "DEBUG/css" ) );
879 linkCSS.forEach(
880 function ( item ) {
881 var srcCSS = srcHTML.create( item );
882 var shortName = Path.basename( srcCSS.getAbsoluteFilePath() );
883 var output = Path.join( cssDir, shortName );
884 PathUtils.file( output, srcCSS.tag( "debug" ) );
885 if ( !head.children ) head.children = [];
886 head.children.push(
887 Tree.tag(
888 "link", {
889 href: shiftPath + "css/" + shortName + seed,
890 rel: "stylesheet",
891 type: "text/css"
892 }
893 )
894 );
895 head.children.push( {
896 type: Tree.TEXT,
897 text: "\n"
898 } );
899 manifestFiles.push( "css/" + shortName );
900 var resources = srcCSS.listResources();
901 resources.forEach(
902 function ( resource ) {
903 var shortName = "css/" + resource[ 0 ];
904 var longName = resource[ 1 ];
905 manifestFiles.push( shortName );
906 this.copyFile( longName, Path.join( this.wwwPath( "DEBUG" ), shortName ) );
907 },
908 this
909 );
910 },
911 this
912 );
913
914 // For type "nodewebkit", all JS must lie in "node_modules" and they
915 // don't need to be declared in the HTML file.
916 var jsDirShortName = ( this._type == 'nodewebkit' ? "node_modules" : "js" );
917 var jsDir = this.mkdir( this.wwwPath( "DEBUG/" + jsDirShortName ) );
918 linkJS.forEach(
919 function ( item ) {
920 var srcJS = srcHTML.create( item );
921 var shortName = Path.basename( srcJS.getAbsoluteFilePath() );
922 var output = Path.join( jsDir, shortName );
923 var code = srcJS.read();
924 if ( item.substr( 0, 4 ) == 'mod/' ) {
925 if ( this._type == 'nodewebkit' ) {
926 // Let's add internationalisation snippet.
927 code = ( srcJS.tag( "intl" ) || "" ) + code;
928 } else {
929 // This is a module. We need to wrap it in module's declaration snippet.
930 code =
931 "require('" +
932 shortName.substr( 0, shortName.length - 3 ).toLowerCase() +
933 "', function(exports, module){\n" +
934 ( srcJS.tag( "intl" ) || "" ) +
935 code +
936 "\n});\n";
937 }
938 }
939 PathUtils.file( output, code );
940 if ( this._type != 'nodewebkit' ) {
941 // Declaration and manifest only needed for project of
942 // type that is not "nodewebkit".
943 if ( !head.children ) head.children = [];
944 head.children.push(
945 Tree.tag(
946 "script", {
947 src: shiftPath + jsDirShortName + "/" + shortName + seed
948 }
949 )
950 );
951 head.children.push( {
952 type: Tree.TEXT,
953 text: "\n"
954 } );
955 manifestFiles.push( jsDirShortName + "/" + shortName );
956 }
957 },
958 this
959 );
960 srcHTML.tag( "resources" ).forEach(
961 function ( itm, idx, arr ) {
962 var src = itm;
963 var dst = src;
964 if ( Array.isArray( src ) ) {
965 dst = src[ 1 ];
966 src = src[ 0 ];
967 }
968 manifestFiles.push( dst );
969 src = this.srcPath( src );
970 dst = Path.join( this.wwwPath( "DEBUG" ), dst );
971 this.copyFile( src, dst );
972 }, this
973 );
974
975 // Adding innerJS and innerCSS.
976 var shortNameJS = PathUtils.addPrefix( filename.substr( 0, filename.length - 5 ), "@" ) + ".js";
977 head.children.push(
978 Tree.tag(
979 "script", {
980 src: shiftPath + jsDirShortName + "/" + shortNameJS + seed
981 }
982 )
983 );
984 manifestFiles.push( jsDirShortName + "/" + shortNameJS );
985 var wwwInnerJS = Path.join( jsDir, shortNameJS );
986 PathUtils.file(
987 wwwInnerJS,
988 srcHTML.tag( "innerJS" )
989 );
990
991 if ( true ) {
992 // For now, we decided to put the CSS relative to the inner HTML into the <head>'s tag.
993 head.children.push(
994 Tree.tag( "style", {}, srcHTML.tag( "innerCSS" ) )
995 );
996 } else {
997 // If we want to externalise the inner CSS in the future, we can use this piece of code.
998 var shortNameCSS = PathUtils.addPrefix( filename.substr( 0, filename.length - 5 ), "@" ) + ".css";
999 head.children.push(
1000 Tree.tag(
1001 "link", {
1002 href: shiftPath + "css/" + shortNameCSS + seed,
1003 rel: "stylesheet",
1004 type: "text/css"
1005 }
1006 )
1007 );
1008 manifestFiles.push( shiftPath + "css/" + shortNameCSS );
1009 PathUtils.file(
1010 Path.join( cssDir, shortNameCSS ),
1011 srcHTML.tag( "innerCSS" )
1012 );
1013 }
1014
1015 if ( this._type != 'nodewebkit' ) {
1016 // Looking for manifest file.
1017 var html = Tree.findChild( tree, "html" );
1018 if ( html ) {
1019 var manifestFilename = Tree.att( "manifest" );
1020 if ( manifestFilename ) {
1021 // Writing manifest file only if needed.
1022 PathUtils.file(
1023 Path.join( this.wwwPath( "DEBUG" ), filename + ".manifest" ),
1024 "CACHE MANIFEST\n" +
1025 "# " + ( new Date() ) + " - " + Date.now() + "\n\n" +
1026 "CACHE:\n" +
1027 manifestFiles.join( "\n" ) +
1028 "\n\nNETWORK:\n*\n"
1029 );
1030 }
1031 }
1032 }
1033 // Writing HTML file.
1034 PathUtils.file(
1035 Path.join( this.wwwPath( "DEBUG" ), filename ),
1036 "<!-- " + ( new Date() ).toString() + " -->" +
1037 "<!DOCTYPE html>" + Tree.toString( tree )
1038 );
1039 // Writing ".htaccess" file.
1040 this.writeHtaccess( "DEBUG" );
1041 // Looking for webapp manifest for Firefox OS (also used for nodewebkit but with another name).
1042 copyManifestWebapp.call( this, "DEBUG" );
1043};
1044
1045/**
1046 * @param mode can be "RELEASE" or "DEBUG".
1047 * @return void
1048 */
1049Project.prototype.writeHtaccess = function ( mode ) {
1050 PathUtils.file(
1051 Path.join( this.wwwPath( mode ), ".htaccess" ),
1052 "AddType application/x-web-app-manifest+json .webapp\n" +
1053 "AddType text/cache-manifest .manifest\n" +
1054 "ExpiresByType text/cache-manifest \"access plus 0 seconds\"\n" +
1055 "Header set Expires \"Thu, 19 Nov 1981 08:52:00 GM\"\n" +
1056 "Header set Cache-Control \"no-store, no-cache, must-revalidate, post-check=0, pre-check=0\"\n" +
1057 "Header set Pragma \"no-cache\"\n"
1058 );
1059};
1060
1061/**
1062 * @param mode : "DEBUG" or "RELEASE".
1063 */
1064function copyManifestWebapp( mode ) {
1065 var filename = "manifest.webapp";
1066 var out;
1067 if ( this._type == 'nodewebkit' ) filename = "package.json";
1068
1069 console.log( "Copying " + filename.cyan + "..." );
1070
1071 // Looking for webapp manifest for Firefox OS.
1072 if ( false == FS.existsSync( this.srcPath( filename ) ) ) {
1073 out = Template.file( filename, this._config ).out;
1074 PathUtils.file( this.srcPath( filename ), out );
1075 }
1076 var webappFile = this.srcPath( filename );
1077 if ( webappFile ) {
1078 var content = FS.readFileSync( webappFile ).toString();
1079 var json = null;
1080 try {
1081 json = JSON.parse( content );
1082 } catch ( x ) {
1083 this.fatal( "'" + filename + "' must be a valid JSON file!\n" + x );
1084 }
1085 json.version = this._config.version;
1086 if ( typeof json.window === 'object' ) {
1087 json.window.toolbar = ( mode == "DEBUG" );
1088 }
1089 PathUtils.file( Path.join( this.wwwPath( mode ), filename ), JSON.stringify( json, null, 4 ) );
1090 var icons = json.icons || {};
1091 var key, val;
1092 for ( key in icons ) {
1093 val = this.srcOrLibPath( icons[ key ] );
1094 if ( val ) {
1095 this.copyFile( val, Path.join( this.wwwPath( mode ), icons[ key ] ) );
1096 }
1097 }
1098 }
1099}
1100
1101/**
1102 * @return array of HTML files found in _srcDir_.
1103 */
1104Project.prototype.findHtmlFiles = function () {
1105 var that = this;
1106
1107 var filters = this._config.tfw.compile.files;
1108 if ( typeof filters === 'undefined' ) filters = "\\.html$";
1109 var files = [],
1110 srcDir = this.srcPath(),
1111 prefixLength = srcDir.length + 1,
1112 filter, i, rxFilters = [],
1113 arr;
1114 if ( !Array.isArray( filters ) ) {
1115 filters = [ filters ];
1116 }
1117 for ( i = 0; i < filters.length; i++ ) {
1118 filter = filters[ i ];
1119 if ( typeof filter !== 'string' ) {
1120 this.fatal( "Invalid atribute \"tfw.compile.files\" in \"package.json\"!\n" +
1121 "Must be a string or an array of strings." );
1122 }
1123 arr = [];
1124 filter.split( "/" ).forEach(
1125 function ( item ) {
1126 try {
1127 item = item.trim();
1128 if ( item == '' || item == '*' ) {
1129 // `null`matches anything.
1130 arr.push( null );
1131 } else {
1132 arr.push( new RegExp( item, "i" ) );
1133 }
1134 } catch ( ex ) {
1135 this.fatal(
1136 "Invalid regular expression for filter: " + JSON.stringify( filter ) + "!"
1137 );
1138 }
1139 }
1140 );
1141 rxFilters.push( arr );
1142 }
1143 rxFilters.forEach(
1144 function ( f ) {
1145 PathUtils.findFiles( srcDir, f ).forEach(
1146 function ( item ) {
1147 if ( item.substr( item.length - 5 ).toLowerCase() !== '.html' ) {
1148 console.log( "Copying " + item.yellow );
1149 that.copyFile( item, that.wwwPath( Path.basename( item ) ) );
1150 return;
1151 }
1152 files.push( item.substr( prefixLength ) );
1153 }
1154 );
1155 }
1156 );
1157 if ( files.length == 0 ) {
1158 this.fatal(
1159 "No HTML file found!\n\nPattern: " + JSON.stringify( filters ) +
1160 "\nFolder: " + srcDir
1161 );
1162 }
1163 return files;
1164};
1165
1166/**
1167 * @param arguments all arguments will be joined to form the path of the directory to create.
1168 * @return the name of the created directory.
1169 */
1170Project.prototype.mkdir = function () {
1171 var key, arg, items = [];
1172 for ( key in arguments ) {
1173 arg = arguments[ key ].trim();
1174 items.push( arg );
1175 }
1176 var path = Path.resolve( Path.normalize( items.join( "/" ) ) ),
1177 item, i,
1178 curPath = "";
1179 items = path.replace( /\\/g, '/' ).split( "/" );
1180 for ( i = 0; i < items.length; i++ ) {
1181 item = items[ i ];
1182 curPath += item + "/";
1183 if ( FS.existsSync( curPath ) ) {
1184 var stat = FS.statSync( curPath );
1185 if ( !stat.isDirectory() ) {
1186 break;
1187 }
1188 } else {
1189 try {
1190 FS.mkdirSync( curPath );
1191 } catch ( ex ) {
1192 throw {
1193 fatal: "Unable to create directory \"" + curPath + "\"!\n" + ex
1194 };
1195 }
1196 }
1197 }
1198 return path;
1199};
1200
1201// Used for file copy.
1202var buffer = new Buffer( 64 * 1024 );
1203
1204/**
1205 * Copy a file from `src` to `dst`.
1206 * @param src full path of the source file.
1207 * @param dst full path of the destination file.
1208 */
1209Project.prototype.copyFile = function ( src, dst, log ) {
1210 if ( log ) {
1211 console.log( "copyFile( " + src.cyan.bold + ", " + dst + " )" );
1212 }
1213 if ( !FS.existsSync( src ) ) {
1214 this.fatal( "Unable to copy missing file: " + src + "\ninto: " + dst, -1, src );
1215 }
1216 var stat = FS.statSync( src );
1217 if ( stat.isDirectory() ) {
1218 // We need to copy a whole directory.
1219 if ( FS.existsSync( dst ) ) {
1220 // Check if the destination is a directory.
1221 stat = FS.statSync( dst );
1222 if ( !stat.isDirectory() ) {
1223 this.fatal( "Destination is not a directory: \"" + dst +
1224 "\"!\nSource is \"" + src + "\".", -1, "project.copyFile" );
1225 }
1226 } else {
1227 // Make destination directory.
1228 this.mkdir( dst );
1229 }
1230 var files = FS.readdirSync( src );
1231 files.forEach(
1232 function ( filename ) {
1233 this.copyFile(
1234 Path.join( src, filename ),
1235 Path.join( dst, filename ),
1236 log
1237 );
1238 },
1239 this
1240 );
1241 return;
1242 }
1243
1244 var bytesRead, pos, rfd, wfd;
1245 this.mkdir( Path.dirname( dst ) );
1246 try {
1247 rfd = FS.openSync( src, "r" );
1248 } catch ( ex ) {
1249 this.fatal( "Unable to open file \"" + src + "\" for reading!\n" + ex, -1, "project.copyFile" );
1250 }
1251 try {
1252 wfd = FS.openSync( dst, "w" );
1253 } catch ( ex ) {
1254 this.fatal( "Unable to open file \"" + dst + "\" for writing!\n" + ex, -1, "project.copyFile" );
1255 }
1256 bytesRead = 1;
1257 pos = 0;
1258 while ( bytesRead > 0 ) {
1259 try {
1260 bytesRead = FS.readSync( rfd, buffer, 0, 64 * 1024, pos );
1261 } catch ( ex ) {
1262 this.fatal( "Unable to read file \"" + src + "\"!\n" + ex, -1, "project.copyFile" );
1263 }
1264 FS.writeSync( wfd, buffer, 0, bytesRead );
1265 pos += bytesRead;
1266 }
1267 FS.closeSync( rfd );
1268 return FS.closeSync( wfd );
1269};
1270
1271
1272/**
1273 * @param {string} prjDir root directory of the project. It is where we can find `project.tfw.json`.
1274 * @return {object} instance of the class `Project`.
1275 */
1276function createProject( prjDir ) {
1277 return new Project( prjDir );
1278}
1279
1280
1281/**
1282 * @param {object} project - Instance of Project.
1283 * @returns {undefined}
1284 */
1285function initializedPrivateVariables( project ) {
1286 // Will own the list of HTML files as Source objects.
1287 project._compiledFiles = [];
1288
1289 /*
1290 * To prevent double flushing of the same file, this array keeps the
1291 * name of the already flushed files.
1292 * @see `this.flushContent()`
1293 */
1294 project._flushedContent = [];
1295
1296 project._modulesPath = [];
1297}
1298
1299
1300/**
1301 * @param {string} version - Version in format `major.minor.patch`.
1302 * @returns {ojbect} `{major, minor, patch}`.
1303 */
1304function parseVersion( version ) {
1305 try {
1306 const
1307 versionArray = version.split( "." ),
1308 MAJOR_INDEX = 0,
1309 MINOR_INDEX = 1,
1310 PATCH_INDEX = 2;
1311 return {
1312 major: versionArray[ MAJOR_INDEX ],
1313 minor: versionArray[ MINOR_INDEX ],
1314 patch: versionArray[ PATCH_INDEX ]
1315 };
1316 } catch ( ex ) {
1317 throw new Error( `Unable to parse version ${JSON.stringify(version, null, ' ')}` );
1318 }
1319}
1320
1321/**
1322 * @param {object} cfg - Configuration.
1323 * @returns {object} The minimal object that we must provide as the module `$.js`.
1324 */
1325function getModuleContentFor$( cfg ) {
1326 const version = parseVersion( cfg.version );
1327 return {
1328 name: JSON.stringify( cfg.name ),
1329 description: JSON.stringify( cfg.description || "" ),
1330 author: JSON.stringify( cfg.author || "" ),
1331 version: JSON.stringify( cfg.version ),
1332 major: version.major,
1333 minor: version.minor,
1334 revision: version.patch,
1335 date: new Date(),
1336 consts: {}
1337 };
1338}
1339
1340
1341/**
1342 * @param {array} htmlFiles - List of all the HTML files we add to compile.
1343 * @param {object} options - Options for debug, release, ...
1344 * @returns {array} List of actually compiled files.
1345 * @see findHtmlFiles
1346 */
1347function compileHtmlFiles( htmlFiles, options ) {
1348 const compiledFiles = [];
1349 for ( let i = 0; i < htmlFiles.length; i++ ) {
1350 const filename = htmlFiles[ i ];
1351 try {
1352 const compiledFile = CompilerHTML2.compile( filename, Util.clone( options ) );
1353 compiledFiles.push( compiledFile );
1354 } catch ( ex ) {
1355 Fatal.bubble( ex, filename );
1356 }
1357 }
1358 return compiledFiles;
1359}
\No newline at end of file