UNPKG

8.57 kBJavaScriptView Raw
1/**
2 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3 * For licensing, see LICENSE.md.
4 */
5
6'use strict';
7
8const chalk = require( 'chalk' );
9const path = require( 'path' );
10const fs = require( 'fs-extra' );
11const createSpinner = require( './tools/create-spinner' );
12
13module.exports = {
14 createSpinner,
15
16 /**
17 * Executes a shell command.
18 *
19 * @param {String} command The command to be executed.
20 * @param {Object} options
21 * @param {String} [options.verbosity='info'] Level of the verbosity. If set as 'info' both outputs (stdout and
22 * stderr) will be logged. If set as 'error', only stderr output will be logged.
23 * @returns {String} The command output.
24 */
25 shExec( command, options = { verbosity: 'info' } ) {
26 const logger = require( './logger' );
27 const log = logger( options.verbosity );
28 const sh = require( 'shelljs' );
29
30 sh.config.silent = true;
31
32 const ret = sh.exec( command );
33
34 const grey = chalk.grey;
35
36 if ( ret.code ) {
37 if ( ret.stdout ) {
38 log.error( grey( ret.stdout ) );
39 }
40
41 if ( ret.stderr ) {
42 log.error( grey( ret.stderr ) );
43 }
44
45 throw new Error( `Error while executing ${ command }: ${ ret.stderr }` );
46 }
47
48 if ( ret.stdout ) {
49 log.info( grey( ret.stdout ) );
50 }
51
52 if ( ret.stderr ) {
53 log.info( grey( ret.stderr ) );
54 }
55
56 return ret.stdout;
57 },
58
59 /**
60 * Links directory located in source path to directory located in destination path.
61 * @param {String} source
62 * @param {String} destination
63 */
64 linkDirectories( source, destination ) {
65 const fs = require( 'fs' );
66 // Remove destination directory if exists.
67 if ( this.isSymlink( destination ) ) {
68 this.removeSymlink( destination );
69 } else if ( this.isDirectory( destination ) ) {
70 this.shExec( `rm -rf ${ destination }` );
71 }
72
73 fs.symlinkSync( source, destination, 'dir' );
74 },
75
76 /**
77 * Returns array with all directories under specified path.
78 *
79 * @param {String} path
80 * @returns {Array}
81 */
82 getDirectories( path ) {
83 const fs = require( 'fs' );
84 const pth = require( 'path' );
85
86 return fs.readdirSync( path ).filter( item => {
87 return this.isDirectory( pth.join( path, item ) );
88 } );
89 },
90
91 /**
92 * Returns true if path points to existing directory.
93 *
94 * @param {String} path
95 * @returns {Boolean}
96 */
97 isDirectory( path ) {
98 const fs = require( 'fs' );
99
100 try {
101 return fs.statSync( path ).isDirectory();
102 } catch ( e ) {
103 return false;
104 }
105 },
106
107 /**
108 * Returns true if path points to existing file.
109 *
110 * @param {String} path
111 * @returns {Boolean}
112 */
113 isFile( path ) {
114 const fs = require( 'fs' );
115
116 try {
117 return fs.statSync( path ).isFile();
118 } catch ( e ) {
119 return false;
120 }
121 },
122
123 /**
124 * Returns true if path points to symbolic link.
125 *
126 * @param {String} path
127 */
128 isSymlink( path ) {
129 const fs = require( 'fs' );
130
131 try {
132 return fs.lstatSync( path ).isSymbolicLink();
133 } catch ( e ) {
134 return false;
135 }
136 },
137
138 /**
139 * Updates JSON file under specified path.
140 * @param {String} path Path to file on disk.
141 * @param {Function} updateFunction Function that will be called with parsed JSON object. It should return
142 * modified JSON object to save.
143 */
144 updateJSONFile( path, updateFunction ) {
145 const fs = require( 'fs' );
146
147 const contents = fs.readFileSync( path, 'utf-8' );
148 let json = JSON.parse( contents );
149 json = updateFunction( json );
150
151 fs.writeFileSync( path, JSON.stringify( json, null, 2 ) + '\n', 'utf-8' );
152 },
153
154 /**
155 * Reinserts all object's properties in alphabetical order (character's Unicode value).
156 * Used for JSON.stringify method which takes keys in insertion order.
157 *
158 * @param { Object } obj
159 * @returns { Object } Same object with sorted keys.
160 */
161 sortObject( obj ) {
162 Object.keys( obj ).sort().forEach( key => {
163 const val = obj[ key ];
164 delete obj[ key ];
165 obj[ key ] = val;
166 } );
167
168 return obj;
169 },
170
171 /**
172 * Returns name of the NPM module located under provided path.
173 *
174 * @param {String} modulePath Path to NPM module.
175 */
176 readPackageName( modulePath ) {
177 const fs = require( 'fs' );
178 const path = require( 'path' );
179 const packageJSONPath = path.join( modulePath, 'package.json' );
180
181 if ( !this.isFile( packageJSONPath ) ) {
182 return null;
183 }
184
185 const contents = fs.readFileSync( packageJSONPath, 'utf-8' );
186 const json = JSON.parse( contents );
187
188 return json.name || null;
189 },
190
191 /**
192 * Calls `npm install` command in specified path.
193 *
194 * @param {String} path
195 */
196 npmInstall( path ) {
197 this.shExec( `cd ${ path } && npm install` );
198 },
199
200 /**
201 * Calls `npm uninstall <name>` command in specified path.
202 *
203 * @param {String} path
204 * @param {String} name
205 */
206 npmUninstall( path, name ) {
207 this.shExec( `cd ${ path } && npm uninstall ${ name }` );
208 },
209
210 /**
211 * Calls `npm update --dev` command in specified path.
212 *
213 * @param {String} path
214 */
215 npmUpdate( path ) {
216 this.shExec( `cd ${ path } && npm update --dev` );
217 },
218
219 /**
220 *
221 * Copies file located under `inputPath` to `outputPath`. Optionally replaces contents of the file using provided
222 * `replace` object.
223 *
224 * // Each occurrence of `{{appName}}` inside README.md will be changed to `ckeditor5`.
225 * tools.copyTemplateFile( '/path/to/README.md', '/new/path/to/README.md', { '{{AppName}}': 'ckeditor5' } );
226 *
227 * @param {String} inputPath Path to input file.
228 * @param {String} outputPath Path to output file.
229 * @param {Object} [replace] Object with data to fill template. Method will take object's keys and replace their
230 * occurrences with value stored under that key.
231 */
232 copyTemplateFile( inputPath, outputPath, replace ) {
233 const fs = require( 'fs-extra' );
234 const path = require( 'path' );
235
236 // Create destination directory if one not exists.
237 fs.ensureDirSync( path.dirname( outputPath ) );
238
239 // If there is nothing to modify - just copy the file.
240 if ( !replace ) {
241 fs.copySync( inputPath, outputPath );
242
243 return;
244 }
245
246 // When replace object is passed - modify file on the fly.
247 const regexpList = [];
248
249 for ( const variableName in replace ) {
250 regexpList.push( variableName );
251 }
252
253 // Create one regexp for all variables to replace.
254 const regexp = new RegExp( regexpList.join( '|' ), 'g' );
255
256 // Read and modify.
257 const inputData = fs.readFileSync( inputPath, 'utf8' );
258 const modifiedData = inputData.replace( regexp, matched => replace[ matched ] );
259
260 // Save.
261 fs.writeFileSync( outputPath, modifiedData, 'utf8' );
262 },
263
264 /**
265 * Copies a file. Takes care of creating parent directory if it doesn't exist.
266 *
267 * @param {String} from Source path.
268 * @param {String} to Destination path (directory with file name).
269 * @returns {Promise}
270 */
271 copyFile( from, to ) {
272 return new Promise( ( resolve, reject ) => {
273 fs.readFile( from, 'utf8', ( err, data ) => {
274 if ( err ) {
275 return reject( err );
276 }
277
278 fs.outputFile( to, data, err => {
279 if ( err ) {
280 return reject( err );
281 }
282
283 return resolve();
284 } );
285 } );
286 } );
287 },
288
289 /**
290 * Executes 'npm view' command for provided module name and returns Git url if one is found. Returns null if
291 * module cannot be found.
292 *
293 * @param {String} name Name of the module.
294 * @returns {*}
295 */
296 getGitUrlFromNpm( name ) {
297 try {
298 const info = JSON.parse( this.shExec( `npm view ${ name } repository --json`, false ) );
299
300 if ( info && info.type == 'git' ) {
301 return info.url;
302 }
303 } catch ( error ) {
304 // Throw error only when different than E404.
305 if ( error.message.indexOf( 'npm ERR! code E404' ) == -1 ) {
306 throw error;
307 }
308 }
309
310 return null;
311 },
312
313 /**
314 * Unlinks symbolic link under specified path.
315 *
316 * @param {String} path
317 */
318 removeSymlink( path ) {
319 const fs = require( 'fs' );
320 fs.unlinkSync( path );
321 },
322
323 /**
324 * Removes files and directories specified by `glob` starting from `rootDir`
325 * and gently informs about deletion.
326 *
327 * @param {String} rootDir The path to the root directory (i.e. "dist/").
328 * @param {String} glob Glob specifying what to clean.
329 * @param {Object} options
330 * @param {String} options.verbosity='info' A level of the verbosity for logger.
331 * @returns {Promise}
332 */
333 clean( rootDir, glob, options = { verbosity: 'info' } ) {
334 const del = require( 'del' );
335
336 const joinedPath = path.join( rootDir, glob );
337 const normalizedPath = joinedPath.split( '\\' ).join( '/' );
338
339 return del( normalizedPath )
340 .then( paths => {
341 const log = require( './logger' )( options.verbosity );
342
343 paths.forEach( p => {
344 log.info( `Deleted file '${ chalk.cyan( p ) }'.` );
345 } );
346 } );
347 }
348};