time-to-botec

Benchmark sampling in different programming languages
Log | Files | Refs | README

manifest.js (8082B)


      1 /**
      2 * @license Apache-2.0
      3 *
      4 * Copyright (c) 2018 The Stdlib Authors.
      5 *
      6 * Licensed under the Apache License, Version 2.0 (the "License");
      7 * you may not use this file except in compliance with the License.
      8 * You may obtain a copy of the License at
      9 *
     10 *    http://www.apache.org/licenses/LICENSE-2.0
     11 *
     12 * Unless required by applicable law or agreed to in writing, software
     13 * distributed under the License is distributed on an "AS IS" BASIS,
     14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     15 * See the License for the specific language governing permissions and
     16 * limitations under the License.
     17 */
     18 
     19 'use strict';
     20 
     21 // MODULES //
     22 
     23 var path = require( 'path' );
     24 var cwd = require( 'process' ).cwd;
     25 var logger = require( 'debug' );
     26 var resolve = require( 'resolve' ).sync;
     27 var parentPath = require( '@stdlib/fs/resolve-parent-path' ).sync;
     28 var convertPath = require( './../../convert-path' );
     29 var isObject = require( './is_object.js' );
     30 var unique = require( './unique.js' );
     31 var validate = require( './validate.js' );
     32 var DEFAULTS = require( './defaults.json' );
     33 
     34 
     35 // VARIABLES //
     36 
     37 var debug = logger( 'library-manifest:main' );
     38 
     39 // NOTE: for the following, we explicitly avoid using stdlib packages in this particular package in order to avoid circular dependencies. This should not be problematic as (1) this package is unlikely to be used outside of Node.js and, thus, in environments lacking support for the built-in APIs, and (2) most of the historical bugs for the respective APIs were in environments such as IE and not the versions of V8 included in Node.js >= v0.10.x.
     40 var hasOwnProp = Object.prototype.hasOwnProperty;
     41 var objectKeys = Object.keys;
     42 
     43 
     44 // MAIN //
     45 
     46 /**
     47 * Returns a configuration.
     48 *
     49 * @param {string} fpath - manifest file path
     50 * @param {Object} conditions - conditions
     51 * @param {Options} [options] - options
     52 * @param {string} [options.basedir] - base search directory
     53 * @param {string} [options.paths] - path convention
     54 * @throws {TypeError} first argument must be a string
     55 * @throws {TypeError} second argument must be an object
     56 * @throws {TypeError} options argument must be a plain object
     57 * @throws {TypeError} must provide valid options
     58 * @returns {Object} configuration
     59 *
     60 * @example
     61 * var conf = manifest( './manifest.json', {} );
     62 */
     63 function manifest( fpath, conditions, options ) {
     64 	var coptnames;
     65 	var mpath;
     66 	var ropts;
     67 	var mopts;
     68 	var conf;
     69 	var opts;
     70 	var deps;
     71 	var obj;
     72 	var key;
     73 	var tmp;
     74 	var err;
     75 	var dir;
     76 	var o;
     77 	var i;
     78 	var j;
     79 	var k;
     80 
     81 	if ( typeof fpath !== 'string' ) {
     82 		throw new TypeError( 'invalid argument. First argument must be a string. Value: `'+fpath+'`.' );
     83 	}
     84 	opts = JSON.parse( JSON.stringify( DEFAULTS ) );
     85 	if ( arguments.length > 2 ) {
     86 		err = validate( opts, options );
     87 		if ( err ) {
     88 			throw err;
     89 		}
     90 		opts.basedir = path.resolve( cwd(), opts.basedir );
     91 	} else {
     92 		opts.basedir = cwd();
     93 	}
     94 	debug( 'Options: %s', JSON.stringify( opts ) );
     95 
     96 	fpath = path.resolve( opts.basedir, fpath );
     97 	dir = path.dirname( fpath );
     98 	debug( 'Manifest file path: %s', fpath );
     99 
    100 	conf = require( fpath ); // eslint-disable-line stdlib/no-dynamic-require
    101 
    102 	// NOTE: Instead of using `@stdlib/utils/copy`, we stringify and then parse the configuration object to create a deep copy in an ES5 environment while avoiding circular dependencies. This assumes that the configuration object is valid JSON.
    103 	conf = JSON.parse( JSON.stringify( conf ) );
    104 	debug( 'Manifest: %s', JSON.stringify( conf ) );
    105 
    106 	// TODO: validate a loaded manifest (conf) according to a JSON schema
    107 
    108 	// Handle input conditions...
    109 	if ( !isObject( conditions ) ) {
    110 		throw new TypeError( 'invalid argument. Second argument must be an object. Value: `' + conditions + '`.' );
    111 	}
    112 	debug( 'Provided conditions: %s', JSON.stringify( conditions ) );
    113 	coptnames = objectKeys( conf.options );
    114 	for ( i = 0; i < coptnames.length; i++ ) {
    115 		key = coptnames[ i ];
    116 		if ( hasOwnProp.call( conditions, key ) ) {
    117 			conf.options[ key ] = conditions[ key ];
    118 		}
    119 	}
    120 	debug( 'Conditions for matching a configuration: %s', JSON.stringify( conf.options ) );
    121 
    122 	// Resolve a configuration based on provided conditions...
    123 	debug( 'Resolving matching configuration.' );
    124 	for ( i = 0; i < conf.confs.length; i++ ) {
    125 		o = conf.confs[ i ];
    126 
    127 		// Require that all conditions must match in order to match a configuration...
    128 		for ( j = 0; j < coptnames.length; j++ ) {
    129 			key = coptnames[ j ];
    130 			if (
    131 				!hasOwnProp.call( o, key ) ||
    132 				o[ key ] !== conf.options[ key ]
    133 			) {
    134 				break;
    135 			}
    136 		}
    137 		// If we exhausted all the options, then we found a match...
    138 		if ( j === coptnames.length ) {
    139 			// NOTE: Instead of using `@stdlib/utils/copy`, we stringify and then parse the object to create a deep copy in an ES5 environment while avoiding circular dependencies. This assumes that the object is valid JSON.
    140 			obj = JSON.parse( JSON.stringify( o ) );
    141 			debug( 'Matching configuration: %s', JSON.stringify( obj ) );
    142 			break;
    143 		}
    144 	}
    145 	if ( obj === void 0 ) {
    146 		debug( 'Unable to resolve a matching configuration.' );
    147 		return {};
    148 	}
    149 	// Resolve manifest file paths...
    150 	for ( i = 0; i < conf.fields.length; i++ ) {
    151 		key = conf.fields[ i ].field;
    152 		if ( hasOwnProp.call( obj, key ) ) {
    153 			o = obj[ key ];
    154 			if ( conf.fields[ i ].resolve ) {
    155 				for ( j = 0; j < o.length; j++ ) {
    156 					o[ j ] = path.resolve( dir, o[ j ] );
    157 				}
    158 			}
    159 		}
    160 	}
    161 	// Resolve dependencies (WARNING: circular dependencies will cause an infinite loop)...
    162 	deps = obj.dependencies;
    163 
    164 	debug( 'Resolving %d dependencies.', deps.length );
    165 	ropts = {
    166 		'basedir': opts.basedir
    167 	};
    168 	for ( i = 0; i < deps.length; i++ ) {
    169 		debug( 'Resolving dependency: %s', deps[ i ] );
    170 
    171 		// Resolve a dependency's main entry point:
    172 		mpath = resolve( deps[ i ], ropts );
    173 		debug( 'Dependency entry point: %s', mpath );
    174 
    175 		// Resolve a dependency's path by finding the dependency's `package.json`:
    176 		mpath = parentPath( 'package.json', {
    177 			'dir': path.dirname( mpath )
    178 		});
    179 		mpath = path.dirname( mpath );
    180 		debug( 'Dependency path: %s', mpath );
    181 
    182 		// Load the dependency configuration (recursive):
    183 		mopts = {
    184 			'basedir': mpath
    185 		};
    186 		o = manifest( path.join( mpath, opts.filename ), conditions, mopts );
    187 		debug( 'Dependency manifest: %s', JSON.stringify( o ) );
    188 
    189 		// Merge each field into the main configuration making sure to resolve file paths (note: we ignore whether a dependency specifies whether to generate relative paths; the only context where relative path generation is considered is the root manifest)...
    190 		debug( 'Merging dependency manifest.' );
    191 		for ( j = 0; j < conf.fields.length; j++ ) {
    192 			key = conf.fields[ j ].field;
    193 			if ( hasOwnProp.call( o, key ) ) {
    194 				tmp = o[ key ];
    195 				if ( conf.fields[ j ].resolve ) {
    196 					for ( k = 0; k < tmp.length; k++ ) {
    197 						tmp[ k ] = path.resolve( mpath, tmp[ k ] );
    198 					}
    199 				}
    200 				obj[ key ] = obj[ key ].concat( tmp );
    201 			}
    202 		}
    203 		debug( 'Resolved dependency: %s', deps[ i ] );
    204 	}
    205 	// Dedupe values (dependencies may share common dependencies)...
    206 	debug( 'Removing duplicate entries.' );
    207 	for ( i = 0; i < conf.fields.length; i++ ) {
    208 		key = conf.fields[ i ].field;
    209 		if ( hasOwnProp.call( obj, key ) ) {
    210 			obj[ key ] = unique( obj[ key ] );
    211 		}
    212 	}
    213 	// Generate relative paths (if specified)...
    214 	debug( 'Generating relative paths.' );
    215 	for ( i = 0; i < conf.fields.length; i++ ) {
    216 		key = conf.fields[ i ].field;
    217 		if (
    218 			hasOwnProp.call( obj, key ) &&
    219 			conf.fields[ i ].resolve &&
    220 			conf.fields[ i ].relative
    221 		) {
    222 			tmp = obj[ key ];
    223 			for ( j = 0; j < tmp.length; j++ ) {
    224 				tmp[ j ] = path.relative( dir, tmp[ j ] );
    225 			}
    226 		}
    227 	}
    228 	// Convert paths to a particular path convention...
    229 	if ( opts.paths ) {
    230 		debug( 'Converting paths to specified convention.' );
    231 		for ( i = 0; i < conf.fields.length; i++ ) {
    232 			key = conf.fields[ i ].field;
    233 			if ( hasOwnProp.call( obj, key ) ) {
    234 				tmp = obj[ key ];
    235 				for ( j = 0; j < tmp.length; j++ ) {
    236 					tmp[ j ] = convertPath( tmp[ j ], opts.paths );
    237 				}
    238 			}
    239 		}
    240 	}
    241 	debug( 'Final configuration: %s', JSON.stringify( obj ) );
    242 	return obj;
    243 }
    244 
    245 
    246 // EXPORTS //
    247 
    248 module.exports = manifest;