1 | ;
|
2 |
|
3 | module.exports = {
|
4 |
|
5 | /**
|
6 | * @param {string} js Script you want to zip.
|
7 | * @return zipped script.
|
8 | */
|
9 | zipJS,
|
10 |
|
11 | /**
|
12 | * @param {string} css Style you want to zip.
|
13 | * @return zipped cascading style sheet.
|
14 | */
|
15 | zipCSS,
|
16 |
|
17 | /**
|
18 | * Return a copy of an array after removing all doubles.
|
19 | * @param {array} arrInput array of any comparable object.
|
20 | * @returns {array} A copy of the input array without doubles.
|
21 | */
|
22 | removeDoubles,
|
23 |
|
24 | /**
|
25 | * Remove all files and directories found in `path`, but not `path` itself.
|
26 | * @param {string} path - The folder we want to clean the content.
|
27 | * @param {boolean} _preserveGit [false] - If `true`, the folder ".git" is not deleted.
|
28 | * @returns {undefined}
|
29 | */
|
30 | cleanDir,
|
31 |
|
32 | /**
|
33 | * @param {array} arr - Array in which we want to find `item`.
|
34 | * @param {any} item - Element to find in `arr`.
|
35 | * @returns {boolean} `true` is `item` is an element of `arr`;
|
36 | */
|
37 | isInArray,
|
38 |
|
39 | /**
|
40 | * @param {array|string} target - Any object providing a `length` method.
|
41 | * @returns {boolean} `true` is and only if `target.length !== 0`.
|
42 | */
|
43 | isEmpty,
|
44 |
|
45 | /**
|
46 | * @param {object} obj - Object to clone.
|
47 | * @returns {object} Copy of `obj`.
|
48 | */
|
49 | clone,
|
50 |
|
51 | /**
|
52 | * @param {string} path - Example: `mod/mymodule.js`.
|
53 | * @param {string} newFirstSubFolder - Example: `css`.
|
54 | * @return {string} We search for the first folder in `path` and
|
55 | * replace it with `newFirstSubFolder`.
|
56 | */
|
57 | replaceFirstSubFolder,
|
58 |
|
59 | /**
|
60 | * @param {string} path - Example: `mod/mymodule.js`.
|
61 | * @return {string} We search for the first folder in `path` and remove it.
|
62 | */
|
63 | removeFirstSubFolder,
|
64 |
|
65 | /**
|
66 | * @param {string} filename - Path of a file.
|
67 | * @param {string} newextension - New extension to give to the file `filename`.
|
68 | * @returns {string} `filename` with extension `newExtension`.
|
69 | */
|
70 | replaceExtension,
|
71 |
|
72 | /**
|
73 | * @param {string} filename - Path of a file.
|
74 | * @returns {string} `filename` without its extension.
|
75 | */
|
76 | removeExtension,
|
77 |
|
78 | /**
|
79 | * @param {number} length - Number of bytes.
|
80 | * @returns {string} Conversion in kilobytes with 3 decimals if less than 3 kb.
|
81 | */
|
82 | convertToKiloBytes,
|
83 |
|
84 | /**
|
85 | * Check if at least one file is newer than the target one.
|
86 | *
|
87 | * @param {array} inputs - Array of files (with full path) to compare to `target`.
|
88 | * @param {string} target - Full path of the reference file.
|
89 | * @returns {Boolean} `true` if `target` does not exist, or if at leat one input is newer than `target`.
|
90 | */
|
91 | isNewerThan,
|
92 |
|
93 | /**
|
94 | * @param {string} filename - Name of the file.
|
95 | * @param {string} extension - Extension.
|
96 | * @returns {boolean} `true` if the filename has the expected extension.
|
97 | */
|
98 | hasExtension,
|
99 |
|
100 | /**
|
101 | * @param {array} arr - Array at the end of which we want to push an unique item.
|
102 | * @param {any} element - The element we want to push, but only if it is not already in.
|
103 | * @returns {array} The initial array.
|
104 | */
|
105 | pushUnique,
|
106 |
|
107 | /**
|
108 | * A module called "tfw.view.textbox" can be stored in two different files:
|
109 | * * "tfw.view.textbox.js"
|
110 | * * "tfw/view/textbox.js"
|
111 | *
|
112 | * Prefer the second syntax because it makes easy to look for modules in a file manager.
|
113 | *
|
114 | * @param {string} path - Syntax with dots.
|
115 | * @returns {string} Syntax with slashes.
|
116 | */
|
117 | replaceDotsWithSlashes
|
118 | };
|
119 |
|
120 | const
|
121 | PathUtils = require( "./pathutils" ),
|
122 | CleanCSS = require( "clean-css" ),
|
123 | UglifyJS = require( "uglify-js" ),
|
124 | Path = require( "path" ),
|
125 | FS = require( "fs" );
|
126 |
|
127 |
|
128 | /**
|
129 | * A module called "tfw.view.textbox" can be stored in two different files:
|
130 | * * "tfw.view.textbox.js"
|
131 | * * "tfw/view/textbox.js"
|
132 | *
|
133 | * Prefer the second syntax because it makes easy to look for modules in a file manager.
|
134 | *
|
135 | * @param {string} path - Syntax with dots.
|
136 | * @returns {string} Syntax with slashes.
|
137 | */
|
138 | function replaceDotsWithSlashes( path ) {
|
139 | if ( typeof path !== 'string' ) return path;
|
140 |
|
141 | const
|
142 | pieces = Path.parse( path ),
|
143 | prefix = Path.join( pieces.root, pieces.dir ),
|
144 | name = pieces.name.split( '.' ).join( Path.sep );
|
145 | return Path.normalize( Path.join( prefix, `${name}${pieces.ext}` ) );
|
146 | }
|
147 |
|
148 |
|
149 | /**
|
150 | * @param {array} arr - Array at the end of which we want to push an unique item.
|
151 | * @param {any} element - The element we want to push, but only if it is not already in.
|
152 | * @returns {array} The initial array.
|
153 | */
|
154 | function pushUnique( arr, element ) {
|
155 | if ( !isInArray( arr, element ) ) arr.push( element );
|
156 | return arr;
|
157 | }
|
158 |
|
159 | /**
|
160 | * @param {number} length - Number of bytes.
|
161 | * @returns {string} Conversion in kilobytes with 3 decimals if less than 3 kb.
|
162 | */
|
163 | function convertToKiloBytes( length ) {
|
164 | const
|
165 | DECIMALS = 3,
|
166 | KILOBYTE = 1024,
|
167 | THRESHOLD = 3 * KILOBYTE;
|
168 | if ( length < THRESHOLD ) return ( length / KILOBYTE ).toFixed( DECIMALS );
|
169 | return Math.ceil( length / KILOBYTE );
|
170 | }
|
171 |
|
172 |
|
173 | /**
|
174 | * @param {string} path - Example: `mod/mymodule.js`.
|
175 | * @param {string} newFirstSubFolder - Example: `css`.
|
176 | * @return {string} We search for the first folder in `path` and
|
177 | * replace it with `newFirstSubFolder`.
|
178 | */
|
179 | function replaceFirstSubFolder( path, newFirstSubFolder ) {
|
180 | const
|
181 | slashPosition = path.indexOf( '/' ),
|
182 | NOT_FOUND = -1;
|
183 | if ( slashPosition === NOT_FOUND ) {
|
184 | return path;
|
185 | }
|
186 | if ( newFirstSubFolder.endsWith( '/' ) ) {
|
187 | return newFirstSubFolder + path.substr( slashPosition + 1 );
|
188 | }
|
189 | return newFirstSubFolder + path.substr( slashPosition );
|
190 | }
|
191 |
|
192 |
|
193 | /**
|
194 | * @param {string} path - Example: `mod/mymodule.js`.
|
195 | * @return {string} We search for the first folder in `path` and remove it.
|
196 | */
|
197 | function removeFirstSubFolder( path ) {
|
198 | const
|
199 | slashPosition = path.indexOf( '/' ),
|
200 | NOT_FOUND = -1;
|
201 | if ( slashPosition === NOT_FOUND ) return path;
|
202 | return path.substr( slashPosition + 1 );
|
203 | }
|
204 |
|
205 | /**
|
206 | * @param {string} filename - Path of a file.
|
207 | * @returns {string} `filename` without its extension.
|
208 | */
|
209 | function removeExtension( filename ) {
|
210 | const
|
211 | dotPosition = filename.lastIndexOf( '.' ),
|
212 | NOT_FOUND = -1;
|
213 | if ( dotPosition === NOT_FOUND ) return filename;
|
214 | return filename.substr( 0, dotPosition );
|
215 | }
|
216 |
|
217 |
|
218 | /**
|
219 | * @param {string} filename - Path of a file.
|
220 | * @param {string} newExtension - New extension to give to the file `filename`.
|
221 | * @returns {string} `filename` with extension `newExtension`.
|
222 | */
|
223 | function replaceExtension( filename, newExtension ) {
|
224 | const
|
225 | dotPosition = filename.lastIndexOf( '.' ),
|
226 | NOT_FOUND = -1;
|
227 | if ( dotPosition === NOT_FOUND ) return filename;
|
228 | if ( newExtension.startsWith( '.' ) ) return filename.substr( 0, dotPosition ) + newExtension;
|
229 | // newExtension has been given without starting dot.
|
230 | return filename.substr( 0, dotPosition + 1 ) + newExtension;
|
231 | }
|
232 |
|
233 | /**
|
234 | * @param {object} obj - Object to clone.
|
235 | * @returns {object} Copy of `obj`.
|
236 | */
|
237 | function clone( obj ) {
|
238 | return JSON.parse( JSON.stringify( obj ) );
|
239 | }
|
240 |
|
241 |
|
242 | /**
|
243 | * @param {array} arr - Array in which we want to find `item`.
|
244 | * @param {any} item - Element to find in `arr`.
|
245 | * @returns {boolean} `true` is `item` is an element of `arr`;
|
246 | */
|
247 | function isInArray( arr, item ) {
|
248 | const NOT_IN_ARRAY = -1;
|
249 | return arr.indexOf( item ) !== NOT_IN_ARRAY;
|
250 | }
|
251 |
|
252 |
|
253 | /**
|
254 | * @param {array|string} target - Any object providing a `length` property.
|
255 | * @returns {boolean} `false` is and only if `target.length === 0`.
|
256 | */
|
257 | function isEmpty( target ) {
|
258 | if ( !target ) return true;
|
259 | if ( typeof target.length !== 'number' ) return true;
|
260 | return target.length !== 0;
|
261 | }
|
262 |
|
263 | /**
|
264 | * @param {string} js Script you want to zip.
|
265 | * @return {string} zipped script.
|
266 | */
|
267 | function zipJS( js ) {
|
268 | try {
|
269 | return UglifyJS.minify( js, { fromString: true } ).code;
|
270 | } catch ( x ) {
|
271 | throwUglifyJSException( js, x );
|
272 | }
|
273 | return null;
|
274 | }
|
275 |
|
276 | /**
|
277 | * @param {string} css Style you want to zip.
|
278 | * @return {string} zipped cascading style sheet.
|
279 | */
|
280 | function zipCSS( css ) {
|
281 | return new CleanCSS( {} ).minify( css );
|
282 | }
|
283 |
|
284 | /**
|
285 | * Return a copy of an array after removing all doubles.
|
286 | * @param {array} arrInput array of any comparable object.
|
287 | * @returns {array} A copy of the input array without doubles.
|
288 | */
|
289 | function removeDoubles( arrInput ) {
|
290 | const
|
291 | arrOutput = [],
|
292 | map = {};
|
293 | arrInput.forEach( function forEachItem( itm ) {
|
294 | if ( itm in map ) return;
|
295 | map[ itm ] = 1;
|
296 | arrOutput.push( itm );
|
297 | } );
|
298 | return arrOutput;
|
299 | }
|
300 |
|
301 | /**
|
302 | * Remove all files and directories found in `path`, but not `path` itself.
|
303 | * @param {string} path - The folder we want to clean the content.
|
304 | * @param {boolean} _preserveGit [false] - If `true`, the folder ".git" is not deleted.
|
305 | * @returns {undefined}
|
306 | */
|
307 | function cleanDir( path, _preserveGit ) {
|
308 | const
|
309 | preserveGit = typeof _preserveGit !== 'undefined' ? _preserveGit : false,
|
310 | fullPath = Path.resolve( path );
|
311 | // If the pah does not exist, everything is fine!
|
312 | if ( !FS.existsSync( fullPath ) ) return;
|
313 |
|
314 | if ( preserveGit ) {
|
315 |
|
316 | /*
|
317 | * We must delete the content of this folder but preserve `.git`.
|
318 | * The `www` dir, for instance, can be used as a `gh-pages` branch.
|
319 | */
|
320 | const files = FS.readdirSync( path );
|
321 | files.forEach( function forEachFile( filename ) {
|
322 | if ( filename === '.git' ) return;
|
323 | const
|
324 | filepath = Path.join( path, filename ),
|
325 | stat = FS.statSync( filepath );
|
326 | if ( stat.isDirectory() ) {
|
327 | if ( filepath !== '.' && filepath !== '..' ) PathUtils.rmdir( filepath );
|
328 | } else FS.unlinkSync( filepath );
|
329 | } );
|
330 | } else {
|
331 | // Brutal clean: remove dir and recreate it.
|
332 | PathUtils.rmdir( path );
|
333 | PathUtils.mkdir( path );
|
334 | }
|
335 | }
|
336 |
|
337 | /**
|
338 | * @class Dependencies
|
339 | */
|
340 | var Resources = function ( data ) {
|
341 | this.clear();
|
342 | this.data( data );
|
343 | };
|
344 |
|
345 | /**
|
346 | * Set/Get the list of dependencies.
|
347 | */
|
348 | Resources.prototype.data = function ( data ) {
|
349 | if ( typeof data === 'undefined' ) {
|
350 | var copy = [];
|
351 | this._data.forEach(
|
352 | function ( itm ) {
|
353 | copy.push( itm );
|
354 | }
|
355 | );
|
356 | return copy;
|
357 | }
|
358 | this.clear();
|
359 | if ( Array.isArray( data ) ) {
|
360 | data.forEach(
|
361 | function ( itm ) {
|
362 | this.add( itm );
|
363 | }, this
|
364 | );
|
365 | }
|
366 | };
|
367 |
|
368 | /**
|
369 | * Remove all the dependencies.
|
370 | */
|
371 | Resources.prototype.clear = function () {
|
372 | this._data = [];
|
373 | this._map = {};
|
374 | };
|
375 |
|
376 | /**
|
377 | * Add a dependency.
|
378 | * @param {string/array} item As an array, it is the couple `[source, destination]`.
|
379 | * If the `source` is the same as the `destination`, just pass one string.
|
380 | */
|
381 | Resources.prototype.add = function ( item ) {
|
382 | var key = item;
|
383 | if ( Array.isArray( item ) ) {
|
384 | key = item[ 0 ];
|
385 | }
|
386 | if ( this._map[ key ] ) return;
|
387 | this._map[ key ] = 1;
|
388 | this._data.push( item );
|
389 | };
|
390 |
|
391 | /**
|
392 | * Loop on the dependencies.
|
393 | */
|
394 | Resources.prototype.forEach = function ( f, that ) {
|
395 | this._data.forEach(
|
396 | function ( itm, idx, arr ) {
|
397 | f( itm, idx, arr );
|
398 | }, that
|
399 | );
|
400 | };
|
401 |
|
402 |
|
403 | exports.Resources = Resources;
|
404 |
|
405 |
|
406 | function throwUglifyJSException( js, ex ) {
|
407 | var msg = ex.message + "\n";
|
408 | msg += " line: " + ex.line + "\n";
|
409 | msg += " col.: " + ex.col + "\n";
|
410 | msg += "----------------------------------------" +
|
411 | "----------------------------------------\n";
|
412 | var content = js;
|
413 | var lines = content.split( "\n" ),
|
414 | lineIndex, indent = '',
|
415 | min = Math.max( 0, ex.line - 1 - 2 ),
|
416 | max = ex.line;
|
417 | for ( lineIndex = min; lineIndex < max; lineIndex++ ) {
|
418 | msg += lines[ lineIndex ].trimRight() + "\n";
|
419 | }
|
420 | for ( lineIndex = 0; lineIndex < ex.col; lineIndex++ ) {
|
421 | indent += ' ';
|
422 | }
|
423 | msg += "\n" + indent + "^\n";
|
424 | throw {
|
425 | fatal: msg,
|
426 | src: "util.zipJS"
|
427 | };
|
428 | }
|
429 |
|
430 | /**
|
431 | * @param {string} filename - Name of the file.
|
432 | * @param {string} extension - Extension.
|
433 | * @returns {boolean} `true` if the filename has the expected extension.
|
434 | */
|
435 | function hasExtension( filename, extension ) {
|
436 | if ( extension.charAt( 0 ) !== '.' ) {
|
437 | return hasExtension( filename, `.${extension}` );
|
438 | }
|
439 | return filename.endsWith( extension );
|
440 | }
|
441 |
|
442 | /**
|
443 | * Check if at least one file is newer than the target one.
|
444 | *
|
445 | * @param {array} inputs - Array of files (with full path) to compare to `target`.
|
446 | * @param {string} target - Full path of the reference file.
|
447 | * @returns {Boolean} `true` if `target` does not exist, or if at leat one input is newer than `target`.
|
448 | */
|
449 | function isNewerThan( inputs, target ) {
|
450 | if ( !FS.existsSync( target ) ) return true;
|
451 |
|
452 | const files = Array.isArray( inputs ) ? inputs : [ inputs ],
|
453 | statTarget = FS.statSync( target ),
|
454 | targetTime = statTarget.mtime;
|
455 |
|
456 | for ( const file of files ) {
|
457 | if ( !FS.existsSync( file ) ) continue;
|
458 |
|
459 | const stat = FS.statSync( file );
|
460 | if ( stat.mtime > targetTime ) {
|
461 | return true;
|
462 | }
|
463 | }
|
464 | return false;
|
465 | }
|