varium
Advanced tools
Comparing version 1.0.1 to 2.0.3
{ | ||
"name": "varium", | ||
"version": "1.0.1", | ||
"description": "A strict parser and validator of environment config variables", | ||
"version": "2.0.3", | ||
"description": "Declare and validate environment variables", | ||
"engines": { | ||
@@ -24,12 +24,19 @@ "node": ">=6.5" | ||
"dependencies": { | ||
"debug": "^2.6.1", | ||
"ramda": "^0.23.0" | ||
"js-yaml": "^3.13.1", | ||
"ramda": "^0.26.1" | ||
}, | ||
"peerDependencies": { | ||
"debug": "2.x" | ||
}, | ||
"devDependencies": { | ||
"chai": "^3.5.0", | ||
"eslint": "^3.15.0", | ||
"eslint-config-airbnb-base": "^11.1.0", | ||
"eslint-plugin-import": "^2.2.0", | ||
"mocha": "^3.0.2" | ||
} | ||
"eslint": "^6.1.0", | ||
"eslint-config-airbnb-base": "^11.2.0", | ||
"eslint-plugin-import": "^2.6.1", | ||
"mocha": "^6.1.4" | ||
}, | ||
"files": [ | ||
"/src" | ||
], | ||
"repository": "ahultgren/node-varium" | ||
} |
148
README.md
# Varium | ||
A strict parser and validator of environment config variables. The fundamental | ||
priniciples are that you want: | ||
Varium is a library and syntax for managing environment variables in a sane way. | ||
You should use it if you want to: | ||
1. to have _one_ place where _all_ environment variables are declared and documented | ||
2. CI and/or builds to _fail_ if any environment variables are missing | ||
3. to prevent developers from ever using an undeclared env var | ||
4. e.g. `SOME_FLAG=false` to be treated like a boolean. | ||
* **declare** all used environment variables in **one place** | ||
* **specify** which **types** they have | ||
* **validate** that they are of the right type | ||
* **cast environment variables** to the right type when used | ||
* **require** certain variables | ||
* **default** to a value for other variables | ||
* **abort CI** if variables are missing or fail validation | ||
* **warn developers** if they use an undeclared environment variable | ||
In short you will never again have to hunt around in the source code for any | ||
envrionment variables you might be missing. | ||
## Installation | ||
(Note that Varium does not handle loading of environment variables from files | ||
(for example in develeopment environments). Use foreman/nf or dotenv, or | ||
whatever you prefer, for that.) | ||
`npm install varium --save` | ||
_Requires node v6.5 or above._ | ||
## Installation | ||
## Usage example | ||
`npm install varium -S` | ||
Create a file called `env.manifest` in the project root. It should contain all | ||
environment variables used in the project. For example: | ||
_Requires node v6.5 or above._ | ||
``` | ||
API_BASE_URL : String | ||
API_SECRET : String | ||
# This is a comment | ||
# The following is an optional variable (the above were required): | ||
NUMBER_OF_ITEMS : Int | | ||
## Usage | ||
FLAG : Bool | False # Variables can also have default values. Here it is False | ||
COMPLEX_VALUE : Json | [{ "object": 42 }] # Use json for advanced data structures | ||
The central piece of your environment configuration is the manifest. I suggest | ||
you create a file named `env.manifest`. It looks like this: | ||
QUOTED_STRING : String | "Quote the string if it contains # or \\escaped chars" | ||
``` | ||
REQUIRED_URL : String | ||
A_NUMBER : Int | 7 | ||
FLAG : Bool | false # Comment, can be used as documentation | ||
LIST_OF_THINGS : Json | [] | ||
OPTIONAL_WITH_UNDEFINED_DEFAULT : String | | ||
``` | ||
Then you need a central file for your config, probably `config/index.js`, where | ||
you need the following: | ||
Then create the file which all your other files imports to obtain the config. | ||
For example `config/index.js`. This needs to at least contain: | ||
```js | ||
const varium = require('varium'); | ||
module.exports = varium(process.env, 'env.manifest'); | ||
module.exports = varium(); | ||
``` | ||
Now you can use the config in other modules. For example: | ||
Import this file in the rest of your project to read environment variables: | ||
```js | ||
const config = require('./config'); | ||
console.log(config.get('A_NUMBER')); // 7 | ||
console.log(config.get('WAIT_WHAT_IS_THIS')); // throws Error: Varium: Undeclared env var "WAIT_WHAT_IS_THIS" | ||
const config = require('../config'); | ||
const url = config.API_BASE_URL; | ||
// An error will be thrown if you try to load an undeclared variable: | ||
const wrong = config.API_BASE_ULR; | ||
// -> Error('Varium: Undeclared env var "API_BASE_ULR.\nMaybe you meant API_BASE_URL?"') | ||
``` | ||
To abort builds, for example on heroku, you can run the config file postbuild. | ||
Just add the following to your package.json: | ||
To prevent other developers or your future self from using `process.env` | ||
directly, use the `no-process-env` | ||
[eslint rule](https://eslint.org/docs/rules/no-process-env). | ||
Your environment now needs to contain the required variables. If you use a | ||
library to load `.env` files (such as node-forman or dotenv), the `.env` could | ||
contain this: | ||
```bash | ||
API_BASE_URL=https://example.com/ | ||
API_SECRET=1337 | ||
NUMBER_OF_ITEMS=3 | ||
``` | ||
To abort builds during CI when environment variables are missing, just run the | ||
config file during th build step. For example, on heroku the following would be | ||
enough: | ||
```js | ||
{ | ||
"scripts": { | ||
"heroku-postbuild": "node config" | ||
"heroku-postbuild": "node ./config" | ||
} | ||
@@ -66,59 +86,3 @@ } | ||
# Documentation | ||
## Manifest syntax | ||
`Varname : Type[ |[ Default]]` | ||
* **Varname**: Name of an environment variable. | ||
* **Type**: The type of the variable. Can be one of `Int, Float, String, Bool, Json` | ||
* **Default** (optional): The default value. Must be the same type as **Type**. | ||
If neither `|` nor a default value is set, the variable will be required. If | ||
only `|` is set, the default will be `undefined` (thus the variable is optional). | ||
For examples see Usage above. | ||
## API | ||
### varium : Env -> PathToManifest -> config | ||
* **Env**: Object String, an object with a key for each environment variable and | ||
the values are strings. | ||
* ** PathToManifest**: String, path to the manifest. Relative to `process.cwd()` | ||
or absolute (e.g. `path.join(__dirname, 'env.manifest')`). | ||
**Returns** an instance of Config. | ||
### config.get : VarName -> value | ||
Takes the name of an environment variable and returns its value. Will throw if | ||
the environment variable is not defined. | ||
* **VarName**: The name of the variable. | ||
**Returns** the value. | ||
[debug]: https://www.npmjs.com/package/debug | ||
### varium.Varium : Config -> varium | ||
* **Config.customValidators** : Use your own validators for custom types, or | ||
overwrite the built-in ones. For example: | ||
```js | ||
Varium({ | ||
FortyTwo : (value, def) => value === "42" ? 42 : def | ||
}, process.env, "env.manifest"); | ||
``` | ||
Returns an instance with the same api as described for `varium` above. | ||
## Logs | ||
[Debug][debug] is used for logging. Thus if you need to debug something, set | ||
`DEBUG=varium:*`. Notice, however, that it's not adviced to use this level in | ||
production. It logs env var values and may thus potentially log secrets, which | ||
is generally frowned upon. | ||
For a complete syntax and api reference (for example how to add your own custom | ||
types), see the [docs](./DOCS.md). |
const fs = require("fs"); | ||
const path = require("path"); | ||
const R = require("ramda"); | ||
const interpret = require("./interpret"); | ||
const validate = require("./validate"); | ||
const nameError = require("./util/suggest"); | ||
const reader = R.curry((config, env, manifestString) => { | ||
const result = validate(config.validators, interpret(manifestString), env); | ||
const errors = result.filter(R.has("error$")).map(R.prop("error$")); | ||
const reader = (config, env, manifestString) => { | ||
const result = validate(config.types, interpret(manifestString), env); | ||
const errors = result.map(x => x.error$).filter(Boolean); | ||
@@ -26,33 +26,40 @@ if (errors.length) { | ||
const values = R.mergeAll(result); | ||
const values = Object.assign.apply(null, [{}].concat(result)); | ||
return { | ||
get: (name) => { | ||
if (Object.prototype.hasOwnProperty.call(values, name)) { | ||
return values[name]; | ||
return new Proxy(values, { | ||
get(target, prop) { | ||
if (!Object.prototype.hasOwnProperty.call(target, prop)) { | ||
if (prop === "get") { | ||
return (name) => { | ||
throw new Error(`Varium upgrade notice: config.get("${name}") is obsolete. Access the property directly using config.${name}`); | ||
}; | ||
} else { | ||
const suggestion = nameError(Object.keys(values), prop); | ||
throw new Error(`Varium: Undeclared env var '${prop}'.\n${suggestion}`); | ||
} | ||
} else { | ||
throw new Error(`Varium: Undeclared env var "${name}"`); | ||
return target[prop]; | ||
} | ||
}, | ||
}; | ||
}); | ||
} | ||
}); | ||
}; | ||
const loader = R.curry((read, manifestPath) => { | ||
const absPath = path.resolve(process.cwd(), manifestPath); | ||
let manifest; | ||
const loader = (manifestPath) => { | ||
const appDir = path.dirname(require.main.filename); | ||
const absPath = path.resolve(appDir, manifestPath); | ||
try { | ||
manifest = fs.readFileSync(absPath, { encoding: "utf8" }); | ||
return fs.readFileSync(absPath, { encoding: "utf8" }); | ||
} catch (e) { | ||
throw new Error(`Varium: Could not find env var manifest at ${absPath}`); | ||
throw new Error(`Varium: Could not find a manifest at ${absPath}`); | ||
} | ||
}; | ||
return read(manifest); | ||
}); | ||
module.exports = ({ | ||
types = {}, | ||
env = process.env, | ||
manifestPath = "env.manifest", | ||
noProcessExit = false, | ||
}) => reader({ types, noProcessExit }, env, loader(manifestPath)); | ||
const Varium = R.curry((validators, env, manifestPath) => | ||
loader(reader(validators, env), manifestPath)); | ||
module.exports = Varium({}); | ||
module.exports.Varium = Varium; | ||
module.exports.reader = reader; |
@@ -1,170 +0,11 @@ | ||
const debug = require("debug"); | ||
const fs = require("fs"); | ||
const path = require("path"); | ||
const yaml = require("js-yaml"); | ||
const parser = require("./syntax-parser"); | ||
const logTokens = debug("varium:lexer:tokens"); | ||
const syntaxPath = path.join(__dirname, "./syntax.yml"); | ||
const syntaxYml = fs.readFileSync(syntaxPath, { encoding: "utf8" }); | ||
const Input = (data) => { | ||
let i = 0; | ||
return { | ||
peek: () => data[i] === undefined ? "\n" : data[i], | ||
pop: () => { | ||
i += 1; | ||
return data[i - 1] === undefined ? "\n" : data[i - 1]; | ||
}, | ||
eof: () => i >= data.length, | ||
}; | ||
}; | ||
const syntax = yaml.safeLoad(syntaxYml); | ||
const State = { | ||
Noop: Symbol("Noop"), | ||
DeclarationName: Symbol("DeclarationName"), | ||
DeclarationSeparator: Symbol("DeclarationSeparator"), | ||
DeclarationType: Symbol("DeclarationType"), | ||
DeclarationEnd: Symbol("DeclarationEnd"), | ||
DeclarationDefaultStart: Symbol("DeclarationDefaultStart"), | ||
QuotedDeclarationDefault: Symbol("QuotedDeclarationDefault"), | ||
DeclarationDefault: Symbol("DeclarationDefault"), | ||
Comment: Symbol("Comment"), | ||
}; | ||
const validDeclarationChar = /[a-zA-Z0-9_]/; | ||
const validComment = /[#]/; | ||
const whiteSpace = /[ \t]/; | ||
const newLine = /[\n]/; | ||
const validDeclarationSeparator = /[:]/; | ||
const validTypeChar = /[a-zA-Z]/; | ||
const validDefaultStart = /[|]/; | ||
const validDefaultChar = /[^#\n|]/; | ||
const validQuote = /["]/; | ||
const escapeChar = /[\\]/; | ||
const validTypeEnd = /[ \t\n|#]/; | ||
module.exports = (chars) => { | ||
const input = Input(chars); | ||
const tokens = []; | ||
let state = State.Noop; | ||
let currentToken = ""; | ||
let escaped = false; | ||
let char = ""; | ||
let keepGoing = true; | ||
while (keepGoing) { | ||
char = input.peek(); | ||
if (input.eof()) { | ||
keepGoing = false; | ||
} | ||
if (state === State.Noop) { | ||
if (char.match(validDeclarationChar)) { | ||
state = State.DeclarationName; | ||
currentToken = ""; | ||
} else if (char.match(validComment)) { | ||
state = State.Comment; | ||
input.pop(); | ||
} else if (char.match(whiteSpace) || char.match(newLine)) { | ||
input.pop(); | ||
} else { | ||
throw new SyntaxError(`Unexpected ${char}`); | ||
} | ||
} else if (state === State.DeclarationName) { | ||
if (char.match(validDeclarationChar)) { | ||
currentToken += input.pop(); | ||
} else if (char.match(whiteSpace) || char.match(validDeclarationSeparator)) { | ||
tokens.push({ | ||
type: "DeclarationName", | ||
value: currentToken, | ||
}); | ||
state = State.DeclarationSeparator; | ||
} else { | ||
throw new SyntaxError(`Unexpected ${char} in DeclarationName`); | ||
} | ||
} else if (state === State.DeclarationSeparator) { | ||
if (char.match(validTypeChar)) { | ||
state = State.DeclarationType; | ||
currentToken = ""; | ||
} else if (char.match(validDeclarationSeparator) || char.match(whiteSpace)) { | ||
// TODO Check multiple chars to ensure " *: *" | ||
input.pop(); | ||
} else { | ||
throw new SyntaxError(`Unexpected ${char} in DeclarationSeparator`); | ||
} | ||
} else if (state === State.DeclarationType) { | ||
if (char.match(validTypeChar)) { | ||
currentToken += input.pop(); | ||
} else if (char.match(validTypeEnd)) { | ||
tokens.push({ | ||
type: "DeclarationType", | ||
value: currentToken, | ||
}); | ||
state = State.DeclarationEnd; | ||
} else { | ||
throw new SyntaxError(`Unexpected char ${char} in DeclarationType`); | ||
} | ||
} else if (state === State.DeclarationEnd) { | ||
if (char.match(whiteSpace)) { | ||
input.pop(); | ||
} else if (char.match(validDefaultStart)) { | ||
input.pop(); | ||
state = State.DeclarationDefaultStart; | ||
} else if (char.match(newLine) || char.match(validComment)) { | ||
state = State.Noop; | ||
} else { | ||
throw new SyntaxError(`Unexpected char ${char} in DeclarationEnd`); | ||
} | ||
} else if (state === State.DeclarationDefaultStart) { | ||
if (char.match(whiteSpace)) { | ||
input.pop(); | ||
} else if (char.match(newLine) || char.match(validComment)) { | ||
tokens.push({ | ||
type: "DeclarationDefault", | ||
value: "", | ||
}); | ||
state = State.Noop; | ||
} else if (char.match(validQuote)) { | ||
state = State.QuotedDeclarationDefault; | ||
currentToken = ""; | ||
input.pop(); | ||
} else if (char.match(validDefaultChar)) { | ||
state = State.DeclarationDefault; | ||
currentToken = ""; | ||
} else { | ||
throw new SyntaxError(`Unexpected char ${char} in DeclarationDefaultStart`); | ||
} | ||
} else if (state === State.QuotedDeclarationDefault) { | ||
if (char.match(validQuote) && !escaped) { | ||
tokens.push({ | ||
type: "DeclarationDefault", | ||
value: currentToken, | ||
}); | ||
state = State.Noop; | ||
input.pop(); | ||
} else if (char.match(escapeChar) && !escaped) { | ||
escaped = true; | ||
input.pop(); | ||
} else { | ||
currentToken += input.pop(); | ||
escaped = false; | ||
} | ||
} else if (state === State.DeclarationDefault) { | ||
if (char.match(validDefaultChar)) { | ||
currentToken += input.pop(); | ||
} else { | ||
state = State.Noop; | ||
tokens.push({ | ||
type: "DeclarationDefault", | ||
value: currentToken.trim(), | ||
}); | ||
} | ||
} else if (state === State.Comment) { | ||
input.pop(); | ||
if (char.match(newLine)) { | ||
state = State.Noop; | ||
} | ||
} else { | ||
throw new SyntaxError(`Seriously wtf just happened? Debug: ${state}`); | ||
} | ||
} | ||
logTokens(tokens); | ||
return tokens; | ||
}; | ||
module.exports = chars => parser(syntax, "Noop", chars); |
@@ -1,38 +0,43 @@ | ||
const R = require("ramda"); | ||
const duplicatedDefinitions = (manifest) => { | ||
const duplicates = []; | ||
const duplicateMap = {}; | ||
const duplicatedDefinitions = R.pipe( | ||
R.groupBy(R.prop("name")), | ||
R.values, | ||
R.filter(R.compose(R.lt(1), R.prop("length"))), | ||
R.map(R.head) | ||
); | ||
module.exports = R.pipe( | ||
R.reduce((definitions, token) => { | ||
/* eslint no-param-reassign: 0 */ | ||
if (token.type === "DeclarationName") { | ||
return [...definitions, { | ||
name: token.value, | ||
}]; | ||
} else if (token.type === "DeclarationType") { | ||
definitions[definitions.length - 1].type = token.value; | ||
} else if (token.type === "DeclarationDefault") { | ||
definitions[definitions.length - 1].default = token.value; | ||
manifest.forEach((definition) => { | ||
if (duplicateMap[definition.name]) { | ||
duplicates.push(definition.name); | ||
} | ||
return definitions; | ||
}, []), | ||
(manifest) => { | ||
const duplicated = duplicatedDefinitions(manifest); | ||
duplicateMap[definition.name] = 1; | ||
}); | ||
if (duplicated.length) { | ||
const stack = duplicated.map(definition => | ||
` Env var ${definition.name} is declared more than once.` | ||
).join("\n"); | ||
const err = new Error(`Varium: Error reading manifest`); | ||
err.stack = stack; | ||
throw err; | ||
} else { | ||
return manifest; | ||
} | ||
return duplicates; | ||
}; | ||
const parseTokens = (definitions, token) => { | ||
/* eslint no-param-reassign: 0 */ | ||
if (token.type === "DeclarationName") { | ||
return [...definitions, { | ||
name: token.value, | ||
}]; | ||
} else if (token.type === "DeclarationType") { | ||
definitions[definitions.length - 1].type = token.value; | ||
} else if (token.type === "DeclarationDefault") { | ||
definitions[definitions.length - 1].default = token.value; | ||
} | ||
); | ||
return definitions; | ||
}; | ||
module.exports = (manifest) => { | ||
const definitions = manifest.reduce(parseTokens, []); | ||
const duplicated = duplicatedDefinitions(definitions); | ||
if (duplicated.length) { | ||
const stack = duplicated.map(definition => | ||
` Env var ${definition.name} is declared more than once.` | ||
).join("\n"); | ||
const err = new Error("Varium: Error reading manifest"); | ||
err.stack = stack; | ||
throw err; | ||
} else { | ||
return definitions; | ||
} | ||
}; |
@@ -1,78 +0,17 @@ | ||
const debug = require("debug"); | ||
const Validators = require("./validators"); | ||
const validatorError = require("../util/suggest"); | ||
const logName = debug("varium:validate:name"); | ||
const logValue = debug("varium:validate:value"); | ||
let logName; | ||
let logValue; | ||
const isnt = x => x === "" || x === undefined; | ||
try { | ||
// eslint-disable-next-line | ||
const debug = require("debug"); | ||
logName = debug("varium:validate:name"); | ||
logValue = debug("varium:validate:value"); | ||
} catch (e) { | ||
logName = () => {}; | ||
logValue = () => {}; | ||
} | ||
const Validators = { | ||
String: (value, def) => value || def, | ||
Int: (value, def) => { | ||
const validDef = isnt(def) ? undefined : parseInt(def, 10); | ||
const validValue = isnt(value) ? undefined : parseInt(value, 10); | ||
if (typeof validDef === "number" && (isNaN(validDef) || String(validDef) !== def)) { | ||
throw new Error("default is not a valid Int"); | ||
} | ||
if (typeof validValue === "number" && (isNaN(validValue) || String(validValue) !== value)) { | ||
throw new Error("value is not a valid Int"); | ||
} | ||
return !isNaN(validValue) | ||
? validValue | ||
: validDef; | ||
}, | ||
Float: (value, def) => { | ||
const validDef = isnt(def) ? undefined : parseFloat(def, 10); | ||
const validValue = isnt(value) ? undefined : parseFloat(value, 10); | ||
if (typeof validDef === "number" && (isNaN(validDef) || isNaN(def))) { | ||
throw new Error("default is not a valid Float"); | ||
} | ||
if (typeof validValue === "number" && (isNaN(validValue) || isNaN(value))) { | ||
throw new Error("value is not a valid Float"); | ||
} | ||
return !isNaN(validValue) | ||
? validValue | ||
: validDef; | ||
}, | ||
Bool: (value, def) => { | ||
let validDef; | ||
let validValue; | ||
if (def === "false") { validDef = false; } | ||
else if (def === "true") { validDef = true; } | ||
else if (isnt(def)) { validDef = undefined; } | ||
else { throw new Error("default is not a valid Bool"); } | ||
if (value === "false") { validValue = false; } | ||
else if (value === "true") { validValue = true; } | ||
else if (isnt(value)) { validValue = undefined; } | ||
else { throw new Error("value is not a valid Bool"); } | ||
return typeof validValue === "boolean" ? validValue : validDef; | ||
}, | ||
Json: (value, def) => { | ||
let validDef; | ||
let validValue; | ||
try { | ||
validDef = isnt(def) ? undefined : JSON.parse(def); | ||
} catch (e) { | ||
throw new Error("default is not a valid Json"); | ||
} | ||
try { | ||
validValue = isnt(value) ? undefined : JSON.parse(value); | ||
} catch (e) { | ||
throw new Error("value is not a valid Json"); | ||
} | ||
return validValue !== undefined ? validValue : validDef; | ||
}, | ||
}; | ||
module.exports = (customValidators, manifest, env) => { | ||
@@ -82,8 +21,15 @@ const validators = Object.assign({}, Validators, customValidators); | ||
return manifest.map((definition) => { | ||
if (!validators[definition.type]) { | ||
const validator = validators[definition.type]; | ||
const envValue = env[definition.name]; | ||
const envDefault = definition.default; | ||
if (!validator) { | ||
const errorMessage = validatorError(Object.keys(validators), definition.type); | ||
return { | ||
error$: `The type ${definition.type} for env var "${definition.name}" does not exist.`, | ||
error$: `The type ${definition.type} for env var "${definition.name}" does not exist.\n${errorMessage}`, | ||
}; | ||
} | ||
if (env[definition.name] === undefined && definition.default === undefined) { | ||
if (envValue === undefined && envDefault === undefined) { | ||
return { | ||
@@ -95,12 +41,26 @@ error$: `Env var "${definition.name}" requires a value.`, | ||
logName(definition.name); | ||
logValue(`Value: ${env[definition.name]}`); | ||
logValue(`Default: ${definition.default}`); | ||
logValue(`Value: ${envValue}`); | ||
logValue(`Default: ${envDefault}`); | ||
if (envDefault !== undefined) { | ||
try { | ||
validator(envDefault); | ||
} catch (e) { | ||
return { | ||
error$: `Default value for "${definition.name}" is invalid: ${e.message}`, | ||
}; | ||
} | ||
} | ||
const value = envValue !== undefined | ||
? envValue | ||
: envDefault; | ||
try { | ||
return { | ||
[definition.name]: validators[definition.type](env[definition.name], definition.default), | ||
[definition.name]: validator(value), | ||
}; | ||
} catch (e) { | ||
return { | ||
error$: `Env var "${definition.name}" is invalid: ${e.message}`, | ||
error$: `Value for "${definition.name}" is invalid: ${e.message}`, | ||
}; | ||
@@ -107,0 +67,0 @@ } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
0
0
15618
3
382
88
3
1
+ Addedjs-yaml@^3.13.1
+ Addedargparse@1.0.10(transitive)
+ Addedesprima@4.0.1(transitive)
+ Addedjs-yaml@3.14.1(transitive)
+ Addedramda@0.26.1(transitive)
+ Addedsprintf-js@1.0.3(transitive)
- Removeddebug@^2.6.1
- Removedramda@0.23.0(transitive)
Updatedramda@^0.26.1