| Leo Repp | 58b9f11 | 2021-11-22 11:57:47 +0100 | [diff] [blame^] | 1 | const fs = require('fs'); |
| 2 | const util = require('util'); |
| 3 | const path = require('path'); |
| 4 | const EE = require('events').EventEmitter; |
| 5 | |
| 6 | const extend = require('extend'); |
| 7 | const resolve = require('resolve'); |
| 8 | const flaggedRespawn = require('flagged-respawn'); |
| 9 | const isPlainObject = require('is-plain-object'); |
| 10 | const mapValues = require('object.map'); |
| 11 | const fined = require('fined'); |
| 12 | |
| 13 | const findCwd = require('./lib/find_cwd'); |
| 14 | const findConfig = require('./lib/find_config'); |
| 15 | const fileSearch = require('./lib/file_search'); |
| 16 | const parseOptions = require('./lib/parse_options'); |
| 17 | const silentRequire = require('./lib/silent_require'); |
| 18 | const buildConfigName = require('./lib/build_config_name'); |
| 19 | const registerLoader = require('./lib/register_loader'); |
| 20 | const getNodeFlags = require('./lib/get_node_flags'); |
| 21 | |
| 22 | |
| 23 | function Liftoff (opts) { |
| 24 | EE.call(this); |
| 25 | extend(this, parseOptions(opts)); |
| 26 | } |
| 27 | util.inherits(Liftoff, EE); |
| 28 | |
| 29 | Liftoff.prototype.requireLocal = function (module, basedir) { |
| 30 | try { |
| 31 | var result = require(resolve.sync(module, {basedir: basedir})); |
| 32 | this.emit('require', module, result); |
| 33 | return result; |
| 34 | } catch (e) { |
| 35 | this.emit('requireFail', module, e); |
| 36 | } |
| 37 | }; |
| 38 | |
| 39 | Liftoff.prototype.buildEnvironment = function (opts) { |
| 40 | opts = opts || {}; |
| 41 | |
| 42 | // get modules we want to preload |
| 43 | var preload = opts.require || []; |
| 44 | |
| 45 | // ensure items to preload is an array |
| 46 | if (!Array.isArray(preload)) { |
| 47 | preload = [preload]; |
| 48 | } |
| 49 | |
| 50 | // make a copy of search paths that can be mutated for this run |
| 51 | var searchPaths = this.searchPaths.slice(); |
| 52 | |
| 53 | // calculate current cwd |
| 54 | var cwd = findCwd(opts); |
| 55 | |
| 56 | // if cwd was provided explicitly, only use it for searching config |
| 57 | if (opts.cwd) { |
| 58 | searchPaths = [cwd]; |
| 59 | } else { |
| 60 | // otherwise just search in cwd first |
| 61 | searchPaths.unshift(cwd); |
| 62 | } |
| 63 | |
| 64 | // calculate the regex to use for finding the config file |
| 65 | var configNameSearch = buildConfigName({ |
| 66 | configName: this.configName, |
| 67 | extensions: Object.keys(this.extensions) |
| 68 | }); |
| 69 | |
| 70 | // calculate configPath |
| 71 | var configPath = findConfig({ |
| 72 | configNameSearch: configNameSearch, |
| 73 | searchPaths: searchPaths, |
| 74 | configPath: opts.configPath |
| 75 | }); |
| 76 | |
| 77 | // if we have a config path, save the directory it resides in. |
| 78 | var configBase; |
| 79 | if (configPath) { |
| 80 | configBase = path.dirname(configPath); |
| 81 | // if cwd wasn't provided explicitly, it should match configBase |
| 82 | if (!opts.cwd) { |
| 83 | cwd = configBase; |
| 84 | } |
| 85 | // resolve symlink if needed |
| 86 | if (fs.lstatSync(configPath).isSymbolicLink()) { |
| 87 | configPath = fs.realpathSync(configPath); |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | // TODO: break this out into lib/ |
| 92 | // locate local module and package next to config or explicitly provided cwd |
| 93 | var modulePath, modulePackage; |
| 94 | try { |
| 95 | var delim = path.delimiter, |
| 96 | paths = (process.env.NODE_PATH ? process.env.NODE_PATH.split(delim) : []); |
| 97 | modulePath = resolve.sync(this.moduleName, {basedir: configBase || cwd, paths: paths}); |
| 98 | modulePackage = silentRequire(fileSearch('package.json', [modulePath])); |
| 99 | } catch (e) {} |
| 100 | |
| 101 | // if we have a configuration but we failed to find a local module, maybe |
| 102 | // we are developing against ourselves? |
| 103 | if (!modulePath && configPath) { |
| 104 | // check the package.json sibling to our config to see if its `name` |
| 105 | // matches the module we're looking for |
| 106 | var modulePackagePath = fileSearch('package.json', [configBase]); |
| 107 | modulePackage = silentRequire(modulePackagePath); |
| 108 | if (modulePackage && modulePackage.name === this.moduleName) { |
| 109 | // if it does, our module path is `main` inside package.json |
| 110 | modulePath = path.join(path.dirname(modulePackagePath), modulePackage.main || 'index.js'); |
| 111 | cwd = configBase; |
| 112 | } else { |
| 113 | // clear if we just required a package for some other project |
| 114 | modulePackage = {}; |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | // load any modules which were requested to be required |
| 119 | if (preload.length) { |
| 120 | // unique results first |
| 121 | preload.filter(function (value, index, self) { |
| 122 | return self.indexOf(value) === index; |
| 123 | }).forEach(function (dep) { |
| 124 | this.requireLocal(dep, findCwd(opts)); |
| 125 | }, this); |
| 126 | } |
| 127 | |
| 128 | var exts = this.extensions; |
| 129 | var eventEmitter = this; |
| 130 | registerLoader(eventEmitter, exts, configPath, cwd); |
| 131 | |
| 132 | var configFiles = {}; |
| 133 | if (isPlainObject(this.configFiles)) { |
| 134 | var notfound = { path: null }; |
| 135 | configFiles = mapValues(this.configFiles, function(prop, name) { |
| 136 | var defaultObj = { name: name, cwd: cwd, extensions: exts }; |
| 137 | return mapValues(prop, function(pathObj) { |
| 138 | var found = fined(pathObj, defaultObj) || notfound; |
| 139 | if (isPlainObject(found.extension)) { |
| 140 | registerLoader(eventEmitter, found.extension, found.path, cwd); |
| 141 | } |
| 142 | return found.path; |
| 143 | }); |
| 144 | }); |
| 145 | } |
| 146 | |
| 147 | return { |
| 148 | cwd: cwd, |
| 149 | require: preload, |
| 150 | configNameSearch: configNameSearch, |
| 151 | configPath: configPath, |
| 152 | configBase: configBase, |
| 153 | modulePath: modulePath, |
| 154 | modulePackage: modulePackage || {}, |
| 155 | configFiles: configFiles |
| 156 | }; |
| 157 | }; |
| 158 | |
| 159 | Liftoff.prototype.handleFlags = function (cb) { |
| 160 | if (typeof this.v8flags === 'function') { |
| 161 | this.v8flags(function (err, flags) { |
| 162 | if (err) { |
| 163 | cb(err); |
| 164 | } else { |
| 165 | cb(null, flags); |
| 166 | } |
| 167 | }); |
| 168 | } else { |
| 169 | process.nextTick(function () { |
| 170 | cb(null, this.v8flags); |
| 171 | }.bind(this)); |
| 172 | } |
| 173 | }; |
| 174 | |
| 175 | Liftoff.prototype.launch = function (opts, fn) { |
| 176 | if (typeof fn !== 'function') { |
| 177 | throw new Error('You must provide a callback function.'); |
| 178 | } |
| 179 | process.title = this.processTitle; |
| 180 | |
| 181 | var completion = opts.completion; |
| 182 | if (completion && this.completions) { |
| 183 | return this.completions(completion); |
| 184 | } |
| 185 | |
| 186 | this.handleFlags(function (err, flags) { |
| 187 | if (err) { |
| 188 | throw err; |
| 189 | } |
| 190 | flags = flags || []; |
| 191 | |
| 192 | var env = this.buildEnvironment(opts); |
| 193 | |
| 194 | var forcedFlags = getNodeFlags.arrayOrFunction(opts.forcedFlags, env); |
| 195 | flaggedRespawn(flags, process.argv, forcedFlags, execute.bind(this)); |
| 196 | |
| 197 | function execute(ready, child, argv) { |
| 198 | if (child !== process) { |
| 199 | var execArgv = getNodeFlags.fromReorderedArgv(argv); |
| 200 | this.emit('respawn', execArgv, child); |
| 201 | } |
| 202 | if (ready) { |
| 203 | fn.call(this, env, argv); |
| 204 | } |
| 205 | } |
| 206 | }.bind(this)); |
| 207 | }; |
| 208 | |
| 209 | |
| 210 | |
| 211 | module.exports = Liftoff; |