diff --git a/service/nodemon.json b/service/nodemon.json new file mode 100644 index 000000000..f34ffcb13 --- /dev/null +++ b/service/nodemon.json @@ -0,0 +1,9 @@ +{ + "execMap": { + "js": "ts-node --transpile-only", + "ts": "ts-node --transpile-only" + }, + "watch": ["src/**/*"], + "ext": "ts,js", + "ignore": ["node_modules"] + } \ No newline at end of file diff --git a/service/npm-shrinkwrap.json b/service/npm-shrinkwrap.json index 69d9178ff..7e8894055 100644 --- a/service/npm-shrinkwrap.json +++ b/service/npm-shrinkwrap.json @@ -58,6 +58,7 @@ "rfc5646": "^3.0.0", "superagent": "^8.0.0", "svg-captcha": "^1.4.0", + "ts-node": "^10.9.2", "uniqid": "^5.2.0", "walk": "2.3.4", "winston": "1.0.1", @@ -1076,6 +1077,26 @@ "node": ">=6.9.0" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -1356,7 +1377,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -1373,8 +1393,7 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -2169,6 +2188,26 @@ "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, "node_modules/@turf/bbox": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-6.3.0.tgz", @@ -3034,7 +3073,6 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -3051,6 +3089,17 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/adm-zip": { "version": "0.4.9", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.9.tgz", @@ -3267,6 +3316,11 @@ "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -4446,6 +4500,11 @@ "sha.js": "^2.4.8" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -8085,6 +8144,11 @@ "semver": "bin/semver.js" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, "node_modules/map-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", @@ -12306,6 +12370,56 @@ "lodash": "^4.17.5" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", @@ -12456,7 +12570,6 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12580,6 +12693,11 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -13086,6 +13204,14 @@ "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/service/package.json b/service/package.json index 36ea77b6e..2421827e5 100644 --- a/service/package.json +++ b/service/package.json @@ -78,6 +78,7 @@ "rfc5646": "^3.0.0", "superagent": "^8.0.0", "svg-captcha": "^1.4.0", + "ts-node": "^10.9.2", "uniqid": "^5.2.0", "walk": "2.3.4", "winston": "1.0.1", diff --git a/service/src/app.ts b/service/src/app.ts index 6e285966a..42af28c5f 100644 --- a/service/src/app.ts +++ b/service/src/app.ts @@ -641,10 +641,10 @@ async function initWebLayer( } } try { - const webappPackagePath = require.resolve('@ngageoint/mage.web-app/package.json') + const webappPackagePath = require.resolve('../../web-app/package.json') const webAppPath = path.dirname(webappPackagePath) - webController.use(express.static(path.join(webAppPath, 'app'))) - webController.use('/admin', express.static(path.join(webAppPath, 'admin'))) + webController.use(express.static(path.join(__dirname, '../../web-app/dist/app'))) + webController.use('/admin', express.static(path.join(__dirname, '../../web-app/dist/admin'))) } catch (err) { console.warn('failed to load mage web app package', err) @@ -705,12 +705,14 @@ declare module 'express' { if (require.main === module) { (async () => { try { - const config = ; + console.log('Starting things...') + const config = JSON.parse(fs.readFileSync(path.resolve('./local_config.js'), 'utf-8'));; const mageService = await boot(config); mageService.open(); console.log('Local development server is running...'); } catch (err) { - + console.log('error starting stuff', err); + process.exit(1) } }) } \ No newline at end of file diff --git a/service/src/local_config.js b/service/src/local_config.js index 7a3a17baf..fd87c2acf 100644 --- a/service/src/local_config.js +++ b/service/src/local_config.js @@ -34,7 +34,7 @@ module.exports = { plugins: { servicePlugins: [ '@ngageoint/mage.arcgis.service', - '@ngageoint/mage.image.service', + '../../plugins/image', '@ngageoint/mage.nga-msi', '@ngageoint/mage.random', ], diff --git a/service/src/mage_local.js b/service/src/mage_local.js new file mode 100755 index 000000000..bfe8a8d6b --- /dev/null +++ b/service/src/mage_local.js @@ -0,0 +1,230 @@ +#!/usr/bin/env node + +const { Command, Option, InvalidArgumentError } = require('commander'); +const path = require('path'); +const _ = require('lodash'); +const magePackage = require('../package.json'); + +const mageCmd = new Command('mage'); +mageCmd.version(magePackage.version); + +const optConfigDesc = + `A JSON document or path to a JSON file or path to a Node module whose export is a MAGE configuration object, e.g., + ${JSON.stringify({ + mage: { + address: '0.0.0.0', + port: 4242, + attachmentDir: '/mage/data/attachments', + mongo: { + poolSize: 2 + } + } + }, null, 2).replace(/\n/g, '\n ')} + +Further individual command line parameters will override those in the configuration document. +`; + +const optPluginsDesc = + `A JSON document or path to a JSON file in the form + ${JSON.stringify({ + servicePlugins: [ + '@my/mage-service-plugin-module' + ], + webUIPlugins: [ + '@my/mage-web-ui-plugin-module' + ] + }, null, 2).replace(/\n/g, '\n ')} + +`; + +const options = [ + new Option('-C --config ', optConfigDesc).env('MAGE_CONFIG').argParser(parseConfigJsonFromStringOrFile), + new Option('-a --address ', 'The address on which the web server listens for HTTP requests').env('MAGE_ADDRESS').default('0.0.0.0'), + new Option('-p --port ', 'The port on which which the web server listens for HTTP requests').env('MAGE_PORT').default(4242).argParser(x => parseInt(x)), + new Option('--attachment-dir ').env('MAGE_ATTACHMENT_DIR').default('/var/lib/mage/attachments'), + new Option('--export-dir ').env('MAGE_EXPORT_DIR').default('/var/lib/mage/export'), + new Option('--icon-dir ').env('MAGE_ICON_DIR').default('/var/lib/mage/icons'), + new Option('--layer-dir ').env('MAGE_LAYER_DIR').default('/var/lib/mage/layers'), + new Option('--security-dir ').env('MAGE_SECURITY_DIR').default('/var/lib/mage/security'), + new Option('--temp-dir ').env('MAGE_TEMP_DIR').default('/tmp'), + new Option('--user-dir ').env('MAGE_USER_DIR').default('/var/lib/mage/users'), + new Option('--export-sweep-interval ').env('MAGE_EXPORT_SWEEP_INTERVAL').default(28800, '8 hours').argParser(x => parseInt(x)), + new Option('--export-ttl ').env('MAGE_EXPORT_TTL').default(259200, '72 hours').argParser(x => parseInt(x)), + new Option('--token-expiration ').env('MAGE_TOKEN_EXPIRATION').default(28800, '8 hours').argParser(x => parseInt(x)), + new Option('--mongo.url ', 'The URL to the MongoDB database, e.g., mongodb://127.0.0.1/mage').env('MAGE_MONGO_URL').default('mongodb://127.0.0.1:27017/magedb'), + new Option('--mongo.conn-timeout ').env('MAGE_MONGO_CONN_TIMEOUT').default(300, '300 seconds').argParser(x => parseInt(x)), + new Option('--mongo.conn-retry-delay ').env('MAGE_MONGO_CONN_RETRY_DELAY').default(5, '5 seconds').argParser(x => parseInt(x)), + new Option('--mongo.pool-size ').env('MAGE_MONGO_POOL_SIZE').default(5).argParser(x => parseInt(x)), + new Option('--mongo.user ').env('MAGE_MONGO_USER'), + new Option('--mongo.password ').env('MAGE_MONGO_PASSWORD'), + new Option('--mongo.ssl [string]').env('MAGE_MONGO_SSL').default(false), + new Option('--mongo.replica-set ').env('MAGE_MONGO_REPLICA_SET'), + new Option('--mongo.x509-key ').env('MAGE_MONGO_X509_KEY'), + new Option('--mongo.x509-key-file ').env('MAGE_MONGO_X509_KEY_FILE'), + new Option('--mongo.x509-cert ').env('MAGE_MONGO_X509_CERT'), + new Option('--mongo.x509-cert-file ').env('MAGE_MONGO_X509_CERT_FILE'), + new Option('--mongo.x509-ca-cert ').env('MAGE_MONGO_X509_CA_CERT'), + new Option('--mongo.x509-ca-cert-file ').env('MAGE_MONGO_X509_CA_CERT_FILE'), + new Option('-P --plugins ', optPluginsDesc).env('MAGE_PLUGINS').argParser(parsePluginsJsonFromStringOrModule), + new Option('--plugin '), + new Option('--web-plugin '), + new Option('--show-config', 'Print the effective configuration and exit without starting the server') +] + +options.forEach(x => mageCmd.addOption(x)) + +mageCmd.parseAsync().then( + async mageCmd => { + const opts = mageCmd.opts(); + const merged = mergeOptsToConfig(opts); + tempEnvHack(merged); + if (merged.showConfig === true) { + console.info(JSON.stringify(merged, null, 2)); + process.exit(0); + } + const { boot } = require('./app'); + const service = await boot(merged); + service.open(); + }, + err => { + console.error(err); + process.exit(1); + } +); + +const configKeyForOptKey = { + exportTtl: 'exportTTL' +}; + +function mergeOptsToConfig(opts) { + const config = opts.config ? opts.config.mage || {} : {}; + const manualMergeKeys = [ 'config', 'plugins', 'plugin', 'webPlugin' ]; + const optsSimple = _.omit(opts, manualMergeKeys); + const DefaultValue = function(x) { + this.value = x; + }; + const optsDefaultsMarked = _.mapValues(optsSimple, (value, key) => { + const optionDef = options.find(x => x.attributeName() === key); + if (optionDef.defaultValue && optionDef.defaultValue === value) { + return new DefaultValue(value); + } + return value; + }); + const optsWithConfigKeys = _.mapKeys(optsDefaultsMarked, (v, k) => configKeyForOptKey[k] || k); + const optsNested = Object.entries(optsWithConfigKeys).reduce((optsNested, entry) => { + if (manualMergeKeys[entry[0]]) { + return optsNested; + } + const nestedEntry = dotsToNested(entry); + return _.merge(optsNested, nestedEntry); + }, {}); + const overriddenDefaults = _.mergeWith({}, config, optsNested, (configValue, optValue, key) => { + if (!optValue) { + return configValue; + } + if (typeof configValue === 'object') { + // nested object - continue recursive merge + return void(0); + } + if (!configValue) { + return optValue instanceof DefaultValue ? optValue.value : void(0); + } + if (optValue instanceof DefaultValue) { + return configValue; + } + if (typeof optValue === 'object') { + // nested object - continue recursive merge + return void(0); + } + return optValue; + }); + const plugins = mergePlugins(opts, config); + return { ...overriddenDefaults, plugins }; +} + +function dotsToNested(entry) { + const steps = entry[0].split('.'); + const nested = steps.slice(0, -1).reduceRight((nested, key) => { + return { [key]: nested }; + }, { [steps.slice(-1)]: entry[1] }); + return nested; +} + +function mergePlugins(opts, config) { + const optsPlugins = opts.plugins || {}; + const configPlugins = config.plugins || {}; + const servicePlugins = (opts.plugin || []).concat(optsPlugins.servicePlugins || []).concat(configPlugins.servicePlugins || []) + const webUIPlugins = (opts.webPlugin || []).concat(optsPlugins.webUIPlugins || []).concat(configPlugins.webUIPlugins || []) + return { servicePlugins, webUIPlugins } +} + +function parseConfigJsonFromStringOrFile(jsonOrPath) { + const config = parseJsonFromStringOrModule(jsonOrPath); + // TODO: validation + return config; +} + +function parsePluginsJsonFromStringOrModule(jsonOrPath) { + const plugins = parseJsonFromStringOrModule(jsonOrPath); + if (!plugins || typeof plugins !== 'object') { + throw new InvalidArgumentError('plugins must be a JSON object'); + } + if (plugins.servicePlugins && !Array.isArray(plugins.servicePlugins)) { + throw new InvalidArgumentError('plugins JSON key servicePlugins must be an array of strings') + } + if (plugins.webUIPlugins && !Array.isArray(plugins.webUIPlugins)) { + throw new InvalidArgumentError('plugins JSON key webUIPlugins must an array of strings') + } + return plugins; +} + +function parseJsonFromStringOrModule(jsonOrModulePath) { + let json = null; + try { + json = JSON.parse(jsonOrModulePath); + } + catch (err) { } + if (!json) { + try { + const jsonPath = path.resolve(process.cwd(), jsonOrModulePath); + json = require(jsonPath); + } + catch (err) { + console.error(err); + throw new InvalidArgumentError(`failed to parse JSON file from path ${jsonOrModulePath}`); + } + } + return json; +} + +/** + * Populate `process.env` with key-value pairs using the environment variable + * names associated with the keys in the given configuration object. This + * supports legacy code that accesses `process.env` directly, or through + * MAGE's `environment/env` module and maintains the behavior of overriding + * environment variables with command line parameters. + * TODO: this will go away when we remove all direct env references + * from downstream code, let commander handle the env vars, and pass + * the configuration object to the mage app + */ +function tempEnvHack(config) { + options.forEach(option => { + const optKey = option.attributeName(); + const configKey = configKeyForOptKey[optKey] || optKey; + const { envVar } = option; + if (!envVar) { + return; + } + const optVal = _.get(config, configKey); + if (typeof optVal === 'object') { + return; + } + if (process.env[envVar] && process.env[envVar] === String(optVal)) { + return; + } + if (!optVal) { + return; + } + process.env[envVar] = String(optVal); + }); +} \ No newline at end of file