diff --git a/node_modules/@npmcli/config/lib/index.js b/node_modules/@npmcli/config/lib/index.js index 8c2b181ca9c61..93fbcad72bc79 100644 --- a/node_modules/@npmcli/config/lib/index.js +++ b/node_modules/@npmcli/config/lib/index.js @@ -296,6 +296,7 @@ class Config { // might be a security hazard, which was the intention. try { this.setCredentialsByURI(reg, creds) + // eslint-disable-next-line no-empty } catch (_) {} process.emit('timeEnd', 'config:load:credentials') @@ -366,9 +367,11 @@ class Config { if (!/^npm_config_/i.test(envKey) || envVal === '') { continue } - const key = envKey.slice('npm_config_'.length) - .replace(/(?!^)_/g, '-') // don't replace _ at the start of the key - .toLowerCase() + let key = envKey.slice('npm_config_'.length) + if (!key.startsWith('//')) { // don't normalize nerf-darted keys + key = key.replace(/(?!^)_/g, '-') // don't replace _ at the start of the key + .toLowerCase() + } conf[key] = envVal } this[_loadObject](conf, 'env', 'environment') @@ -654,6 +657,7 @@ class Config { // saved back to the .npmrc file, so we're good. try { this.setCredentialsByURI(reg, creds) + // eslint-disable-next-line no-empty } catch (_) {} } @@ -691,8 +695,6 @@ class Config { this.delete(`_password`, 'user') this.delete(`username`, 'user') } - this.delete(`${nerfed}:-authtoken`, 'user') - this.delete(`${nerfed}:_authtoken`, 'user') this.delete(`${nerfed}:_authToken`, 'user') this.delete(`${nerfed}:_auth`, 'user') this.delete(`${nerfed}:_password`, 'user') @@ -732,8 +734,6 @@ class Config { // send auth if we have it, only to the URIs under the nerf dart. this.delete(`${nerfed}:always-auth`, 'user') - this.delete(`${nerfed}:-authtoken`, 'user') - this.delete(`${nerfed}:_authtoken`, 'user') this.delete(`${nerfed}:email`, 'user') if (certfile && keyfile) { this.set(`${nerfed}:certfile`, certfile, 'user') @@ -781,8 +781,6 @@ class Config { } const tokenReg = this.get(`${nerfed}:_authToken`) || - this.get(`${nerfed}:_authtoken`) || - this.get(`${nerfed}:-authtoken`) || nerfed === nerfDart(this.get('registry')) && this.get('_authToken') if (tokenReg) { diff --git a/node_modules/@npmcli/config/node_modules/.bin/nopt b/node_modules/@npmcli/config/node_modules/.bin/nopt new file mode 120000 index 0000000000000..6b6566ea7febb --- /dev/null +++ b/node_modules/@npmcli/config/node_modules/.bin/nopt @@ -0,0 +1 @@ +../nopt/bin/nopt.js \ No newline at end of file diff --git a/node_modules/@npmcli/config/node_modules/nopt/LICENSE b/node_modules/@npmcli/config/node_modules/nopt/LICENSE new file mode 100644 index 0000000000000..19129e315fe59 --- /dev/null +++ b/node_modules/@npmcli/config/node_modules/nopt/LICENSE @@ -0,0 +1,15 @@ +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/node_modules/@npmcli/config/node_modules/nopt/bin/nopt.js b/node_modules/@npmcli/config/node_modules/nopt/bin/nopt.js new file mode 100755 index 0000000000000..bb04291c607ac --- /dev/null +++ b/node_modules/@npmcli/config/node_modules/nopt/bin/nopt.js @@ -0,0 +1,56 @@ +#!/usr/bin/env node +var nopt = require('../lib/nopt') +var path = require('path') +var types = { num: Number, + bool: Boolean, + help: Boolean, + list: Array, + 'num-list': [Number, Array], + 'str-list': [String, Array], + 'bool-list': [Boolean, Array], + str: String, + clear: Boolean, + config: Boolean, + length: Number, + file: path, +} +var shorthands = { s: ['--str', 'astring'], + b: ['--bool'], + nb: ['--no-bool'], + tft: ['--bool-list', '--no-bool-list', '--bool-list', 'true'], + '?': ['--help'], + h: ['--help'], + H: ['--help'], + n: ['--num', '125'], + c: ['--config'], + l: ['--length'], + f: ['--file'], +} +var parsed = nopt(types + , shorthands + , process.argv + , 2) + +console.log('parsed', parsed) + +if (parsed.help) { + console.log('') + console.log('nopt cli tester') + console.log('') + console.log('types') + console.log(Object.keys(types).map(function M (t) { + var type = types[t] + if (Array.isArray(type)) { + return [t, type.map(function (mappedType) { + return mappedType.name + })] + } + return [t, type && type.name] + }).reduce(function (s, i) { + s[i[0]] = i[1] + return s + }, {})) + console.log('') + console.log('shorthands') + console.log(shorthands) +} diff --git a/node_modules/@npmcli/config/node_modules/nopt/lib/nopt.js b/node_modules/@npmcli/config/node_modules/nopt/lib/nopt.js new file mode 100644 index 0000000000000..5829c2fe0f637 --- /dev/null +++ b/node_modules/@npmcli/config/node_modules/nopt/lib/nopt.js @@ -0,0 +1,515 @@ +// info about each config option. + +var debug = process.env.DEBUG_NOPT || process.env.NOPT_DEBUG + ? function () { + console.error.apply(console, arguments) + } + : function () {} + +var url = require('url') +var path = require('path') +var Stream = require('stream').Stream +var abbrev = require('abbrev') +var os = require('os') + +module.exports = exports = nopt +exports.clean = clean + +exports.typeDefs = + { String: { type: String, validate: validateString }, + Boolean: { type: Boolean, validate: validateBoolean }, + url: { type: url, validate: validateUrl }, + Number: { type: Number, validate: validateNumber }, + path: { type: path, validate: validatePath }, + Stream: { type: Stream, validate: validateStream }, + Date: { type: Date, validate: validateDate }, + } + +function nopt (types, shorthands, args, slice) { + args = args || process.argv + types = types || {} + shorthands = shorthands || {} + if (typeof slice !== 'number') { + slice = 2 + } + + debug(types, shorthands, args, slice) + + args = args.slice(slice) + var data = {} + var argv = { + remain: [], + cooked: args, + original: args.slice(0), + } + + parse(args, data, argv.remain, types, shorthands) + // now data is full + clean(data, types, exports.typeDefs) + data.argv = argv + Object.defineProperty(data.argv, 'toString', { value: function () { + return this.original.map(JSON.stringify).join(' ') + }, + enumerable: false }) + return data +} + +function clean (data, types, typeDefs) { + typeDefs = typeDefs || exports.typeDefs + var remove = {} + var typeDefault = [false, true, null, String, Array] + + Object.keys(data).forEach(function (k) { + if (k === 'argv') { + return + } + var val = data[k] + var isArray = Array.isArray(val) + var type = types[k] + if (!isArray) { + val = [val] + } + if (!type) { + type = typeDefault + } + if (type === Array) { + type = typeDefault.concat(Array) + } + if (!Array.isArray(type)) { + type = [type] + } + + debug('val=%j', val) + debug('types=', type) + val = val.map(function (v) { + // if it's an unknown value, then parse false/true/null/numbers/dates + if (typeof v === 'string') { + debug('string %j', v) + v = v.trim() + if ((v === 'null' && ~type.indexOf(null)) + || (v === 'true' && + (~type.indexOf(true) || ~type.indexOf(Boolean))) + || (v === 'false' && + (~type.indexOf(false) || ~type.indexOf(Boolean)))) { + v = JSON.parse(v) + debug('jsonable %j', v) + } else if (~type.indexOf(Number) && !isNaN(v)) { + debug('convert to number', v) + v = +v + } else if (~type.indexOf(Date) && !isNaN(Date.parse(v))) { + debug('convert to date', v) + v = new Date(v) + } + } + + if (!Object.prototype.hasOwnProperty.call(types, k)) { + return v + } + + // allow `--no-blah` to set 'blah' to null if null is allowed + if (v === false && ~type.indexOf(null) && + !(~type.indexOf(false) || ~type.indexOf(Boolean))) { + v = null + } + + var d = {} + d[k] = v + debug('prevalidated val', d, v, types[k]) + if (!validate(d, k, v, types[k], typeDefs)) { + if (exports.invalidHandler) { + exports.invalidHandler(k, v, types[k], data) + } else if (exports.invalidHandler !== false) { + debug('invalid: ' + k + '=' + v, types[k]) + } + return remove + } + debug('validated v', d, v, types[k]) + return d[k] + }).filter(function (v) { + return v !== remove + }) + + // if we allow Array specifically, then an empty array is how we + // express 'no value here', not null. Allow it. + if (!val.length && type.indexOf(Array) === -1) { + debug('VAL HAS NO LENGTH, DELETE IT', val, k, type.indexOf(Array)) + delete data[k] + } else if (isArray) { + debug(isArray, data[k], val) + data[k] = val + } else { + data[k] = val[0] + } + + debug('k=%s val=%j', k, val, data[k]) + }) +} + +function validateString (data, k, val) { + data[k] = String(val) +} + +function validatePath (data, k, val) { + if (val === true) { + return false + } + if (val === null) { + return true + } + + val = String(val) + + var isWin = process.platform === 'win32' + var homePattern = isWin ? /^~(\/|\\)/ : /^~\// + var home = os.homedir() + + if (home && val.match(homePattern)) { + data[k] = path.resolve(home, val.slice(2)) + } else { + data[k] = path.resolve(val) + } + return true +} + +function validateNumber (data, k, val) { + debug('validate Number %j %j %j', k, val, isNaN(val)) + if (isNaN(val)) { + return false + } + data[k] = +val +} + +function validateDate (data, k, val) { + var s = Date.parse(val) + debug('validate Date %j %j %j', k, val, s) + if (isNaN(s)) { + return false + } + data[k] = new Date(val) +} + +function validateBoolean (data, k, val) { + if (val instanceof Boolean) { + val = val.valueOf() + } else if (typeof val === 'string') { + if (!isNaN(val)) { + val = !!(+val) + } else if (val === 'null' || val === 'false') { + val = false + } else { + val = true + } + } else { + val = !!val + } + data[k] = val +} + +function validateUrl (data, k, val) { + // Changing this would be a breaking change in the npm cli + /* eslint-disable-next-line node/no-deprecated-api */ + val = url.parse(String(val)) + if (!val.host) { + return false + } + data[k] = val.href +} + +function validateStream (data, k, val) { + if (!(val instanceof Stream)) { + return false + } + data[k] = val +} + +function validate (data, k, val, type, typeDefs) { + // arrays are lists of types. + if (Array.isArray(type)) { + for (let i = 0, l = type.length; i < l; i++) { + if (type[i] === Array) { + continue + } + if (validate(data, k, val, type[i], typeDefs)) { + return true + } + } + delete data[k] + return false + } + + // an array of anything? + if (type === Array) { + return true + } + + // Original comment: + // NaN is poisonous. Means that something is not allowed. + // New comment: Changing this to an isNaN check breaks a lot of tests. + // Something is being assumed here that is not actually what happens in + // practice. Fixing it is outside the scope of getting linting to pass in + // this repo. Leaving as-is for now. + /* eslint-disable-next-line no-self-compare */ + if (type !== type) { + debug('Poison NaN', k, val, type) + delete data[k] + return false + } + + // explicit list of values + if (val === type) { + debug('Explicitly allowed %j', val) + // if (isArray) (data[k] = data[k] || []).push(val) + // else data[k] = val + data[k] = val + return true + } + + // now go through the list of typeDefs, validate against each one. + var ok = false + var types = Object.keys(typeDefs) + for (let i = 0, l = types.length; i < l; i++) { + debug('test type %j %j %j', k, val, types[i]) + var t = typeDefs[types[i]] + if (t && ( + (type && type.name && t.type && t.type.name) ? + (type.name === t.type.name) : + (type === t.type) + )) { + var d = {} + ok = t.validate(d, k, val) !== false + val = d[k] + if (ok) { + // if (isArray) (data[k] = data[k] || []).push(val) + // else data[k] = val + data[k] = val + break + } + } + } + debug('OK? %j (%j %j %j)', ok, k, val, types[types.length - 1]) + + if (!ok) { + delete data[k] + } + return ok +} + +function parse (args, data, remain, types, shorthands) { + debug('parse', args, data, remain) + + var abbrevs = abbrev(Object.keys(types)) + var shortAbbr = abbrev(Object.keys(shorthands)) + + for (var i = 0; i < args.length; i++) { + var arg = args[i] + debug('arg', arg) + + if (arg.match(/^-{2,}$/)) { + // done with keys. + // the rest are args. + remain.push.apply(remain, args.slice(i + 1)) + args[i] = '--' + break + } + var hadEq = false + if (arg.charAt(0) === '-' && arg.length > 1) { + var at = arg.indexOf('=') + if (at > -1) { + hadEq = true + var v = arg.slice(at + 1) + arg = arg.slice(0, at) + args.splice(i, 1, arg, v) + } + + // see if it's a shorthand + // if so, splice and back up to re-parse it. + var shRes = resolveShort(arg, shorthands, shortAbbr, abbrevs) + debug('arg=%j shRes=%j', arg, shRes) + if (shRes) { + debug(arg, shRes) + args.splice.apply(args, [i, 1].concat(shRes)) + if (arg !== shRes[0]) { + i-- + continue + } + } + arg = arg.replace(/^-+/, '') + var no = null + while (arg.toLowerCase().indexOf('no-') === 0) { + no = !no + arg = arg.slice(3) + } + + if (abbrevs[arg]) { + arg = abbrevs[arg] + } + + var argType = types[arg] + var isTypeArray = Array.isArray(argType) + if (isTypeArray && argType.length === 1) { + isTypeArray = false + argType = argType[0] + } + + var isArray = argType === Array || + isTypeArray && argType.indexOf(Array) !== -1 + + // allow unknown things to be arrays if specified multiple times. + if ( + !Object.prototype.hasOwnProperty.call(types, arg) && + Object.prototype.hasOwnProperty.call(data, arg) + ) { + if (!Array.isArray(data[arg])) { + data[arg] = [data[arg]] + } + isArray = true + } + + var val + var la = args[i + 1] + + var isBool = typeof no === 'boolean' || + argType === Boolean || + isTypeArray && argType.indexOf(Boolean) !== -1 || + (typeof argType === 'undefined' && !hadEq) || + (la === 'false' && + (argType === null || + isTypeArray && ~argType.indexOf(null))) + + if (isBool) { + // just set and move along + val = !no + // however, also support --bool true or --bool false + if (la === 'true' || la === 'false') { + val = JSON.parse(la) + la = null + if (no) { + val = !val + } + i++ + } + + // also support "foo":[Boolean, "bar"] and "--foo bar" + if (isTypeArray && la) { + if (~argType.indexOf(la)) { + // an explicit type + val = la + i++ + } else if (la === 'null' && ~argType.indexOf(null)) { + // null allowed + val = null + i++ + } else if (!la.match(/^-{2,}[^-]/) && + !isNaN(la) && + ~argType.indexOf(Number)) { + // number + val = +la + i++ + } else if (!la.match(/^-[^-]/) && ~argType.indexOf(String)) { + // string + val = la + i++ + } + } + + if (isArray) { + (data[arg] = data[arg] || []).push(val) + } else { + data[arg] = val + } + + continue + } + + if (argType === String) { + if (la === undefined) { + la = '' + } else if (la.match(/^-{1,2}[^-]+/)) { + la = '' + i-- + } + } + + if (la && la.match(/^-{2,}$/)) { + la = undefined + i-- + } + + val = la === undefined ? true : la + if (isArray) { + (data[arg] = data[arg] || []).push(val) + } else { + data[arg] = val + } + + i++ + continue + } + remain.push(arg) + } +} + +function resolveShort (arg, shorthands, shortAbbr, abbrevs) { + // handle single-char shorthands glommed together, like + // npm ls -glp, but only if there is one dash, and only if + // all of the chars are single-char shorthands, and it's + // not a match to some other abbrev. + arg = arg.replace(/^-+/, '') + + // if it's an exact known option, then don't go any further + if (abbrevs[arg] === arg) { + return null + } + + // if it's an exact known shortopt, same deal + if (shorthands[arg]) { + // make it an array, if it's a list of words + if (shorthands[arg] && !Array.isArray(shorthands[arg])) { + shorthands[arg] = shorthands[arg].split(/\s+/) + } + + return shorthands[arg] + } + + // first check to see if this arg is a set of single-char shorthands + var singles = shorthands.___singles + if (!singles) { + singles = Object.keys(shorthands).filter(function (s) { + return s.length === 1 + }).reduce(function (l, r) { + l[r] = true + return l + }, {}) + shorthands.___singles = singles + debug('shorthand singles', singles) + } + + var chrs = arg.split('').filter(function (c) { + return singles[c] + }) + + if (chrs.join('') === arg) { + return chrs.map(function (c) { + return shorthands[c] + }).reduce(function (l, r) { + return l.concat(r) + }, []) + } + + // if it's an arg abbrev, and not a literal shorthand, then prefer the arg + if (abbrevs[arg] && !shorthands[arg]) { + return null + } + + // if it's an abbr for a shorthand, then use that + if (shortAbbr[arg]) { + arg = shortAbbr[arg] + } + + // make it an array, if it's a list of words + if (shorthands[arg] && !Array.isArray(shorthands[arg])) { + shorthands[arg] = shorthands[arg].split(/\s+/) + } + + return shorthands[arg] +} diff --git a/node_modules/@npmcli/config/node_modules/nopt/package.json b/node_modules/@npmcli/config/node_modules/nopt/package.json new file mode 100644 index 0000000000000..a3cd13d8c714b --- /dev/null +++ b/node_modules/@npmcli/config/node_modules/nopt/package.json @@ -0,0 +1,53 @@ +{ + "name": "nopt", + "version": "6.0.0", + "description": "Option parsing for Node, supporting types, shorthands, etc. Used by npm.", + "author": "GitHub Inc.", + "main": "lib/nopt.js", + "scripts": { + "preversion": "npm test", + "postversion": "npm publish", + "prepublishOnly": "git push origin --follow-tags", + "test": "tap", + "lint": "eslint \"**/*.js\"", + "postlint": "template-oss-check", + "template-oss-apply": "template-oss-apply --force", + "lintfix": "npm run lint -- --fix", + "snap": "tap", + "posttest": "npm run lint" + }, + "repository": { + "type": "git", + "url": "https://github.com/npm/nopt.git" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "devDependencies": { + "@npmcli/eslint-config": "^3.0.1", + "@npmcli/template-oss": "3.5.0", + "tap": "^16.3.0" + }, + "tap": { + "lines": 87, + "functions": 91, + "branches": 81, + "statements": 87 + }, + "files": [ + "bin/", + "lib/" + ], + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "templateOSS": { + "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", + "windowsCI": false, + "version": "3.5.0" + } +} diff --git a/node_modules/@npmcli/config/package.json b/node_modules/@npmcli/config/package.json index 2f561c12233dc..275044e4ae48e 100644 --- a/node_modules/@npmcli/config/package.json +++ b/node_modules/@npmcli/config/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/config", - "version": "4.2.0", + "version": "4.2.1", "files": [ "bin/", "lib/" @@ -38,7 +38,7 @@ "@npmcli/map-workspaces": "^2.0.2", "ini": "^3.0.0", "mkdirp-infer-owner": "^2.0.0", - "nopt": "^5.0.0", + "nopt": "^6.0.0", "proc-log": "^2.0.0", "read-package-json-fast": "^2.0.3", "semver": "^7.3.5", diff --git a/package-lock.json b/package-lock.json index 167f06964af58..d31e204ebb96d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -90,7 +90,7 @@ "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/arborist": "^5.0.4", "@npmcli/ci-detect": "^2.0.0", - "@npmcli/config": "^4.2.0", + "@npmcli/config": "^4.2.1", "@npmcli/fs": "^2.1.0", "@npmcli/map-workspaces": "^2.0.3", "@npmcli/package-json": "^2.0.0", @@ -867,15 +867,15 @@ } }, "node_modules/@npmcli/config": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@npmcli/config/-/config-4.2.0.tgz", - "integrity": "sha512-imWNz5dNWb2u+y41jyxL2WB389tkhu3a01Rchn16O/ur6GrnKySgOqdNG3N/9Z+mqxdISMEGKXI/POCauzz0dA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@npmcli/config/-/config-4.2.1.tgz", + "integrity": "sha512-iJEnXNAGGr7sGUcoKmeJNrc943vFiWrDWq6DNK/t+SuqoObmozMb3tN3G5T9yo3uBf5Cw4h+SWgoqSaiwczl0Q==", "inBundle": true, "dependencies": { "@npmcli/map-workspaces": "^2.0.2", "ini": "^3.0.0", "mkdirp-infer-owner": "^2.0.0", - "nopt": "^5.0.0", + "nopt": "^6.0.0", "proc-log": "^2.0.0", "read-package-json-fast": "^2.0.3", "semver": "^7.3.5", @@ -885,6 +885,21 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@npmcli/config/node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "inBundle": true, + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/@npmcli/disparity-colors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@npmcli/disparity-colors/-/disparity-colors-2.0.0.tgz", diff --git a/package.json b/package.json index 6efd43f49e96f..5f345a43edf84 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/arborist": "^5.0.4", "@npmcli/ci-detect": "^2.0.0", - "@npmcli/config": "^4.2.0", + "@npmcli/config": "^4.2.1", "@npmcli/fs": "^2.1.0", "@npmcli/map-workspaces": "^2.0.3", "@npmcli/package-json": "^2.0.0", diff --git a/test/lib/commands/adduser.js b/test/lib/commands/adduser.js index 7c7079394f132..94e58a6d3cb0e 100644 --- a/test/lib/commands/adduser.js +++ b/test/lib/commands/adduser.js @@ -34,9 +34,7 @@ t.test('simple login', async t => { '_authtoken=user', '-authtoken=user', '_authToken=user', - '//registry.npmjs.org/:-authtoken=user', '//registry.npmjs.org/:_authToken=user', - '//registry.npmjs.org/:_authtoken=user', '//registry.npmjs.org/:always-auth=user', '//registry.npmjs.org/:email=test-email-old@npmjs.org', ].join('\n'),