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;