UNPKG

13.2 kBJavaScriptView Raw
1"use strict";
2
3module.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
120const
121PathUtils = require( "./pathutils" ),
122CleanCSS = require( "clean-css" ),
123UglifyJS = require( "uglify-js" ),
124Path = require( "path" ),
125FS = 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 */
138function 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 */
154function 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 */
163function 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 */
179function 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 */
197function 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 */
209function 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 */
223function 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 */
237function 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 */
247function 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 */
257function 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 */
267function 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 */
280function 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 */
289function 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 */
307function 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 */
340var Resources = function ( data ) {
341 this.clear();
342 this.data( data );
343};
344
345/**
346 * Set/Get the list of dependencies.
347 */
348Resources.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 */
371Resources.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 */
381Resources.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 */
394Resources.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
403exports.Resources = Resources;
404
405
406function 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 */
435function 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 */
449function 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}