New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

mekano

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mekano - npm Package Compare versions

Comparing version 0.0.5 to 0.0.6

bin/clean.js

5

bin/aliases.js

@@ -49,6 +49,7 @@ 'use strict';

if (!aliases.hasOwnProperty(name)) continue
console.log(' %s %s', helpers.padRight(name, max)
, aliases[name].ast.desc)
var desc = aliases[name].ast.desc
if (desc === null) desc = ''
console.log(' %s %s', helpers.padRight(name, max), desc)
}
console.log()
}

20

bin/cli.js

@@ -31,7 +31,12 @@ #!/usr/bin/env node

}
process.stdout.on('error', function (err) {
if (err.code !== 'EPIPE') throw err
})
var ev = Commands[command](opts)
var errored = false
var finished = false
var signal = null
ev.on('error', function (err) {
log('error', err)
if (err.signal) signal = err.signal
errored = true

@@ -42,4 +47,4 @@ }).on('warning', function (err) {

finished = true
if (errored)
process.exit(1)
if (signal) return process.kill(process.pid, signal)
if (errored) return process.exit(1)
process.exit(0)

@@ -70,12 +75,5 @@ })

Commands.aliases = require('./aliases')
Commands.clean = require('./clean')
Commands.print = require('./print')
Commands.clean = function () {
}
Commands.trace = function () {
}
Commands.help = function () {

@@ -82,0 +80,0 @@ var ev = new EventEmitter()

@@ -6,6 +6,4 @@ 'use strict';

var glob = require('glob')
var util = require('util')
var EventEmitter = require('events').EventEmitter
var map = require('../lib/graph/map')
var ast = require('../lib/read/ast')
var sort = require('../lib/update/sort')

@@ -18,7 +16,4 @@ var expandCmds = require('../lib/update/expand-cmds')

var errors = require('../lib/errors')
var extractCliRefs = require('./extract-cli-refs')
var NO_MATCH = 'no file matches the pattern `%s\''
var NO_SUCH_FILE = 'no such file `%s\', put patterns into "quotes" to avoid ' +
'shell expansion'
function refreshGraph(data) {

@@ -41,3 +36,3 @@ if (!data) throw errors.invalidArg('data', data)

var ev = new EventEmitter()
var et = extractCliRefs(data)
var et = extractCliRefs(data.graph, data.cliRefs)
forwardEvents(ev, et, function cliRefGot(errored, files) {

@@ -62,33 +57,1 @@ if (errored) return ev.emit('finish')

}
function extractCliRefs(data) {
var ev = new EventEmitter()
process.nextTick(function () {
var files = []
if (data.cliRefs.length === 0) {
return ev.emit('finish', data.graph.files)
}
data.cliRefs.forEach(function (ref) {
var err
if (!(ref instanceof ast.Ref) || ref.isA(ast.Ref.ALIAS))
throw errors.invalidArg('data', data)
if (ref.isA(ast.Ref.PATH_GLOB)) {
var newFiles = data.graph.getFilesByGlob(ref.value)
if (newFiles.length === 0) {
err = new Error(util.format(NO_MATCH, ref.value))
return ev.emit('warning', err)
}
files = files.concat(newFiles)
return
}
var newFile = data.graph.getFile(ref.value)
if (newFile === null) {
err = new Error(util.format(NO_SUCH_FILE, ref.value))
return ev.emit('error', err)
}
files.push(newFile)
})
ev.emit('finish', files)
})
return ev
}

@@ -11,3 +11,3 @@ 'use strict';

var ev = new EventEmitter()
var rg = readGraph(opts.file, common.LOG_PATH)
var rg = readGraph(opts.file, common.LOG_PATH, opts.argv.remain)
return forwardEvents(ev, rg, function (errored, data) {

@@ -14,0 +14,0 @@ if (errored) return ev.emit('finish')

@@ -12,3 +12,2 @@ 'use strict';

var EventEmitter = require('events').EventEmitter
var queuedFnOf = require('../lib/queued-fn-of')
var runEdges = require('../lib/update/run-edges')

@@ -19,15 +18,30 @@ var forwardEvents = require('../lib/forward-events')

var helpers = require('./helpers')
var errors = require('../lib/errors')
var SOME_UTD = 'Those are already up-to-date: %s.'
var SOME_UTD = 'Those are already up-to-date: %s'
var CMD_FAIL = 'command failed, code %d: %s'
var CMD_SIGFAIL = 'command failed, signal %s: %s'
var DRY_REM_ORPHAN = 'Would remove orphan: %s'
var REM_ORPHAN = 'Removing orphan: %s'
var SIGS = ['SIGINT', 'SIGHUP', 'SIGTERM', 'SIGQUIT']
function updateGraph(data, opts) {
if (!opts) opts = {}
var ev = new EventEmitter()
forwardEvents(ev, update(data, opts), function () {
if (opts['dry-run']) return ev.emit('finish')
mkdirp(path.dirname(common.LOG_PATH), function (err) {
if (err) return helpers.bailoutEv(ev, err)
var s = data.log.save(fs.createWriteStream(common.LOG_PATH))
s.end(function () {
ev.emit('finish')
unlinkOrphans(data, opts, function (err) {
if (err) return helpers.bailoutEv(ev, err)
var uev = update(data, opts)
var sigInfo = registerSigs(function onSignal(signal) {
uev.emit('signal', signal)
})
forwardEvents(ev, uev, function () {
unregisterSigs(sigInfo)
if (opts['dry-run']) return ev.emit('finish')
mkdirp(path.dirname(common.LOG_PATH), function (err) {
if (err) return helpers.bailoutEv(ev, err)
var s = data.log.save(fs.createWriteStream(common.LOG_PATH))
s.end(function () {
ev.emit('finish')
})
})

@@ -40,2 +54,3 @@ })

function update(data, opts) {
var res
var ev = new EventEmitter()

@@ -45,8 +60,17 @@ if (opts['robot']) console.log(' e %d', data.edges.length)

var st = {data: data, runCount: 0, opts: opts
, sigints: {}, stopFns: {}
, dirs: {}, output: new Output(opts['dry-run'])}
st.updateMessage = opts['robot'] ? updateRobotMessage : updateMessage
var reFn = opts['dry-run'] ? dryRunEdge : runEdge
var re = queuedFnOf(reFn.bind(null, st), os.cpus().length)
var re = reFn.bind(null, st)
st.updateMessage(st, null)
var res = runEdges(data.edges, re)
res = runEdges(data.edges, re, os.cpus().length)
ev.on('signal', function (signal) {
if (signal !== 'SIGINT') {
res.emit('signal', signal)
for (var j in st.stopFns) st.stopFns[j]()
return
}
for (var i in st.sigints) st.sigints[i] = true
})
return forwardEvents(ev, res, function (errored) {

@@ -62,2 +86,76 @@ st.output.endUpdate()

function runEdge(st, edge, cb) {
st.sigints[edge.index] = false
var cmd = st.data.cmds[edge.index]
mkEdgeDirs(st, edge, function (err) {
if (err) return cb(err)
var stopped = false
exec(cmd, function (err, stdout, stderr) {
if (stopped) return
delete st.stopFns[edge.index]
var sigint = st.sigints[edge.index]
delete st.sigints[edge.index]
if (!err) st.runCount++
st.updateMessage(st, edge)
if (stdout.length > 0 || stderr.length > 0) {
st.output.endUpdate()
process.stdout.write(stdout)
process.stderr.write(stderr)
}
if (err) {
var message
if (err.code) message = util.format(CMD_FAIL, err.code, cmd)
else message = util.format(CMD_SIGFAIL, err.signal, cmd)
var nerr = new Error(message)
if ((err.signal === 'SIGINT' || err.code === 130) && sigint)
nerr.signal = 'SIGINT'
return cb(nerr)
}
edge.outFiles.forEach(function (file) {
st.data.log.update(file.path, st.data.imps[file.path])
})
return cb(null)
})
st.stopFns[edge.index] = function stopRunEdge () {
delete st.stopFns[edge.index]
stopped = true
st.updateMessage(st, edge)
return cb(new Error('aborting, recipe process will detach'))
}
})
}
function dryRunEdge(st, edge, cb) {
process.nextTick(function () {
st.runCount++
st.updateMessage(st, edge)
return cb(null)
})
}
function unlinkOrphans(data, opts, cb) {
if (typeof cb !== 'function') throw errors.invalidArg('cb', cb)
var orphans = data.log.getPaths().filter(function (filePath) {
var file = data.graph.getFile(filePath)
return file === null
})
if (orphans.length === 0) return process.nextTick(cb.bind(null, null))
var unlink = opts['dry-run'] ? dryUnlink : fs.unlink
var msgTpl = opts['dry-run'] ? DRY_REM_ORPHAN : REM_ORPHAN
;(function next(i) {
if (i === orphans.length) return cb(null)
if (!opts.robot)
console.log(util.format(msgTpl, orphans[i]))
unlink(orphans[i], function (err) {
if (err) return cb(err)
data.log.forget(orphans[i])
return next(i + 1)
})
})(0)
}
function dryUnlink(filePath, cb) {
setImmediate(cb.bind(null, null))
}
function alreadyUpToDate(ev, data, opts) {

@@ -80,29 +178,14 @@ if (opts['robot']) {

function dryRunEdge(st, edge, cb) {
process.nextTick(function () {
st.runCount++
st.updateMessage(st, edge)
return cb(null)
function registerSigs(fn) {
var info = {sigs: {}}
SIGS.forEach(function (sig) {
var bfn = info.sigs[sig] = fn.bind(null, sig)
process.on(sig, bfn)
})
return info
}
function runEdge(st, edge, cb) {
var cmd = st.data.cmds[edge.index]
mkEdgeDirs(st, edge, function (err) {
if (err) return cb(err)
exec(cmd, function (err, stdout, stderr) {
if (!err) st.runCount++
st.updateMessage(st, edge)
if (stdout.length > 0 || stderr.length > 0) {
st.output.endUpdate()
process.stdout.write(stdout)
process.stderr.write(stderr)
}
if (err)
return cb(new Error(util.format('command failed: %s', cmd)))
edge.outFiles.forEach(function (file) {
st.data.log.update(file.path, st.data.imps[file.path])
})
return cb(null)
})
function unregisterSigs(info) {
SIGS.forEach(function (sig) {
process.removeListener(sig, info.sigs[sig])
})

@@ -109,0 +192,0 @@ }

@@ -19,3 +19,3 @@ 'use strict';

var ev = new EventEmitter()
var rg = readGraph(opts.file, common.LOG_PATH)
var rg = readGraph(opts.file, common.LOG_PATH, opts.argv.remain)
forwardEvents(ev, rg, function graphRead(errored, data) {

@@ -25,3 +25,3 @@ if (errored) return ev.emit('finish')

forwardEvents(ev, ug, function graphUpdated() {
forwardEvents(ev, watchAndUpdate(data))
forwardEvents(ev, watchAndUpdate(data, opts))
})

@@ -32,3 +32,3 @@ })

function watchAndUpdate(data) {
function watchAndUpdate(data, opts) {
var ev = new EventEmitter()

@@ -51,2 +51,3 @@ var patterns = getSourcePatterns(data.transs)

if (err) return helpers.bailoutEv(ev, err)
if (!opts.robot) console.error('Watching...')
this.on('all', function (event, filePath) {

@@ -53,0 +54,0 @@ if (truce) return

@@ -148,5 +148,5 @@ 'use strict';

function fileListComparator(a, b) {
if (a.filePath > b.filePath) return 1
if (a.filePath < b.filePath) return -1
if (a.path > b.path) return 1
if (a.path < b.path) return -1
return 0
}

@@ -70,1 +70,12 @@ 'use strict';

}
Interpolation.prototype.toString = function () {
var str = ''
this._parts.forEach(function (part) {
if (part.val)
str += '$(' + part.str + ')'
else
str += part.str
})
return str
}

@@ -22,2 +22,10 @@ 'use strict';

Scope.prototype.getPairs = function () {
var list = []
for (var name in this._values) {
list.push({name: name, value: this._values[name]})
}
return list
}
Scope.fromBinds = function (binds, parent) {

@@ -24,0 +32,0 @@ var scope = new Scope(parent)

@@ -6,2 +6,3 @@ 'use strict';

var Scope = require('../scope.js')
var errors = require('../errors')

@@ -19,13 +20,13 @@ function expandCmds(scope, recipes, edges) {

if (!recipes.hasOwnProperty(recipeName))
throw new Error(util.format('unknown recipe `%s\'', recipeName))
throw errors.bind(util.format('unknown recipe `%s\'', recipeName))
var interpol = recipes[recipeName].command
var scope = new Scope(unitScope)
scope.set('in', edge.inFiles.map(function (file) {
return file.path
}).join(' '))
scope.set('out', edge.outFiles.map(function (file) {
return file.path
}).join(' '))
scope.set('in', edge.inFiles.map(escapedPathOf).join(' '))
scope.set('out', edge.outFiles.map(escapedPathOf).join(' '))
var command = interpol.expand(scope)
return command
}
function escapedPathOf(file) {
return file.path.replace(/([^\w.\/-])/g, '\\$1')
}

@@ -5,2 +5,3 @@ 'use strict';

var concat = require('concat-stream')
var errors = require('../errors')

@@ -67,2 +68,3 @@ function Log(imps, opts) {

Log.prototype.isGenerated = function (path) {
if (typeof path !== 'string') throw errors.invalidArg('path', path)
return this._imps.hasOwnProperty(path)

@@ -72,2 +74,4 @@ }

Log.prototype.isUpToDate = function (path, imp) {
if (typeof path !== 'string') throw errors.invalidArg('path', path)
if (typeof imp !== 'number') throw errors.invalidArg('imp', imp)
if (!this._imps.hasOwnProperty(path)) return false

@@ -78,3 +82,18 @@ return imp === this._imps[path]

Log.prototype.update = function (path, imp) {
if (typeof path !== 'string') throw errors.invalidArg('path', path)
if (typeof imp !== 'number') throw errors.invalidArg('imp', imp)
this._imps[path] = imp
}
Log.prototype.forget = function (path) {
if (typeof path !== 'string') throw errors.invalidArg('path', path)
delete this._imps[path]
}
Log.prototype.getPaths = function () {
var paths = []
for (var path in this._imps) {
paths.push(path)
}
return paths
}
'use strict';
module.exports = runEdges
var util = require('util')
var EventEmitter = require('events').EventEmitter
function runEdges(edges, runEdge) {
var SIG_ABORT = 'aborting because of signal: %s'
function runEdges(edges, runEdge, maxPending) {
var st = {
runEdge: runEdge
, edges: edges
, events: new EventEmitter()
, done: {}
}
var signal = null
var task = new EventEmitter()
task.on('signal', function (sig) { signal = sig })
for (var k = 0; k < edges.length; ++k)
st.done[edges[k].index] = false
var pending = 0
var queued = []
;(function nextEdges(edges) {
queued = queued.concat(edges)
edges = queued.splice(0, maxPending - pending)
edges.forEach(function forEdge(edge) {

@@ -21,14 +29,21 @@ pending++

pending--
if (!err) st.done[edge.index] = true
if (err) {
st.events.emit('error', err)
if (pending === 0) st.events.emit('finish')
return
task.emit('error', err)
if (err.signal) signal = err.signal
}
st.done[edge.index] = true
nextEdges(getReadyOutEdges(st, edge.outFiles))
if (signal === null)
return nextEdges(getReadyOutEdges(st, edge.outFiles))
if (pending > 0) return
if (signal !== null) {
var nerr = new Error(util.format(SIG_ABORT, signal))
nerr.signal = signal
task.emit('error', nerr)
}
task.emit('finish')
})
})
if (pending === 0) return st.events.emit('finish')
if (pending === 0) return task.emit('finish')
})(getReadyEdges(st, edges))
return st.events
return task
}

@@ -35,0 +50,0 @@

{
"name": "mekano",
"version": "0.0.5",
"version": "0.0.6",
"description": "maintain, update and regenerate groups of files",

@@ -5,0 +5,0 @@ "main": "index.js",

@@ -67,2 +67,8 @@ # ![mekano](https://cdn.mediacru.sh/0hecryCVR3vS.svg)

$ mekano clean
Removing: dist/concat.min.js
Removing: dist/concat.js
Removing: build/foo.js
Removing: build/bar.js
Description

@@ -113,7 +119,9 @@ -----------

npm install mekano
$ npm install mekano
The tool will be available as `node_modules/.bin/mekano`. It is not recommended
to install it globally, because different projects may need different major
and incompatible versions.
to install it globally, because different projects may need different,
incompatible versions (of course, [semver](http://semver.org/) is used). Is
**is** recommended, however, to add it to your project's `package.json` (just
use npm's [`--save` option](https://www.npmjs.org/doc/cli/npm-install.html)).

@@ -137,31 +145,31 @@ To avoid typing the path every time when installed locally, one decent solution

* **update** Update the specified targets. The whole project is updated if no
* `update` Update the specified targets. The whole project is updated if no
target is specified.
* **watch** Keep updating files until a signal is caught. It watches files and
* `watch` Keep updating files until a signal is caught. It watches files and
updates targets when prerequisites change. If the Mekanofile itself changes,
you need to relaunch mekano manually.
* **status** Display the modified files and dirty targets. No target is
updated. If **--silent** is specified, return a zero exit value if the
* `status` Display the modified files and dirty targets. No target is
updated. If `--silent` is specified, return a zero exit value if the
targets are up to date; otherwise, return 1.
* **clean** Remove the specified and dependent targets. For example, with the
* `clean` Remove the specified and dependent targets. For example, with the
above Mekanofile, `clean dist/concat.js` will remove this file and the
minified one. All generated files are removed if no target is specified.
* **aliases** Display a list of the defined aliases.
* **print** *type* Display the mekanofile interpretation. Types:
* **manifest** Output the mekanofile as it had been parsed.
* **dot** Output the file graph in the graphviz dot format.
* **help** Display mekano own help.
* `aliases` Display a list of the defined aliases.
* `print <type>` Display the mekanofile interpretation. Types:
* `manifest` Output the mekanofile as it had been parsed.
* `dot` Output the file graph in the graphviz dot format.
* `help` Display mekano own help.
General options:
* **-y, --shy** Stop an update as soon as an error occurs. By default,
* `-y, --shy` Stop an update as soon as an error occurs. By default,
the update continues to get a maximum of errors at once.
* **-n, --dry-run** Output commands that would be run. No target is updated
* `-n, --dry-run` Output commands that would be run. No target is updated
nor deleted.
* **-f, --file** *mekanofile* Specify a different mekanofile. If '-' is
* `-f, --file <mekanofile>` Specify a different mekanofile. If `-` is
specified, the standard input is used.
* **-r, --robot** Output machine-parseable text.
* **-s, --silent** Be silent: don't write executed commands.
* **-F, --force** Force things, like overwriting modified files. Dangerous.
* **-v, --version** Output version and exit.
* `-r, --robot` Output machine-parseable text.
* `-s, --silent` Be silent: don't write executed commands.
* `-F, --force` Force things, like overwriting modified files. Dangerous.
* `-v, --version` Output version and exit.

@@ -172,15 +180,22 @@ Binds and target names can be mixed on the command-line, but targets are always

Without the option **-f**, *mekano* looks in sequence for the files
**./Mekanofile** and **./mekanofile**. The first found is read.
Without the option `-f`, *mekano* looks in sequence for the files
`./Mekanofile` and `./mekanofile`. The first found is read.
The standard output reports the recipes being executed as well as the completion
percentage. The **-r** option makes the output more easily parseable.
percentage. The `-r` option makes the output more easily parseable.
If any of the SIGHUP, SIGTERM, SIGINT, and SIGQUIT signals is received, the
tool returns cleanly and keeps track of updated files so far.
If any of the SIGHUP, SIGTERM, and SIGQUIT signals is received, the tool stops
updating but keeps track of updated files so far. However, it does not kill the
running sub-processes. If SIGINT (generally Ctrl+C) is received, it follows a
[*"wait and cooperative exit"* (WCE)](http://www.cons.org/cracauer/sigint.html)
stategy; it waits for processes to complete, and stops only if they ended on
SIGINT themselves. In **watch** mode, SIGINT only stops the update, if any; a
second SIGINT may be needed to quit. For compatibility with sloppy signal
handlers, return code 130 is also considered a SIGINT.
At the moment, Mekano cannot update the Mekanofile itself and take account of it
in a single run (with a relation such as `Mekanofile.in M4 -> Mekanofile`). This
is because it won't reload the Mekanofile after its update. You need to run it
twice in this case. This will be improved in the future.
At the moment, Mekano cannot update the Mekanofile itself **and** take account
of it in a single run (with a relation such as
`Mekanofile.in M4 -> Mekanofile`). This is because it won't reload the
Mekanofile after its update. You need to run it twice in this case. This will be
improved in the future.

@@ -207,6 +222,5 @@ Syntax

recipe = recipe-name, ":", command, bind-list, ";" ;
recipe = recipe-name, ":", command, ";" ;
recipe-name = identifier ;
command = interpolation ;
bind-list = [ "{", { bind } , "}" ]
identifier = { ? A-Z, a-z, 0-9, '-' or '_' ? }

@@ -228,11 +242,5 @@ interpolation = "`", ? any character ?, "`" ;

* **in** Space-separated shell-quoted list of the input file(s).
* **out** Space-separated shell-quoted list of the output file(s).
* `in` Space-separated shell-quoted list of the input file(s).
* `out` Space-separated shell-quoted list of the output file(s).
<!--
A recipe can also bind local values with braces, for example:
Compile: `$cc -c $in -o $out` { cc = `gcc $cflags` };
-->
Command lines are evaluated by the local shell, typically with `sh -c`.

@@ -247,3 +255,3 @@ 'UpperCamel' case is suggested for naming recipes. Recipes can appear anywhere

relation = ref-list, { transformation }, [ alias ], ";"
transformation = recipe-name, ( "=>" | "->" ), ref-list, bind-list
transformation = recipe-name, ( "=>" | "->" ), ref-list
alias = "::", alias-name, [ alias-description ]

@@ -264,2 +272,6 @@ ref-list = { path | path-glob | alias-name }

It means: *"takes all the C files in the `source` folder, compile them to object
files in `obj`; then link all those into a single binary `./hello_world`. This
binary can be referred to as the alias `all`."*
#### Expansions

@@ -321,3 +333,3 @@

Generative pattern transposition is planned to be improved in the future.
Generative pattern transposition is planned to be hugely improved in the future.

@@ -353,2 +365,3 @@ ### Binds

to the update log generated by the previous run of *mekano*, if any.
* Remove *orphans*: old generated files that are not in the graph anymore.
* Invoke recipes in order to update files. When possible, recipes are

@@ -360,2 +373,15 @@ called asynchronously to make the update faster.

The update log is located in `.mekano/log.json` and contains the imprint of
each generated file. **Never delete the log file!** To do a whole rebuild, use
the **clean** command instead. If you delete the log, bad things will
happen, because *mekano* will consider all the generated files so far as
sources. This means, for example, that minified files (`foo.min.js`) will be
minified again (`foo.min.min.js`).
When you are using a version control tool, like **git(1)**: if you are
adding built files to the repository (and not just the sources), then you
shall add the log file as well. Otherwise, people checking out the repo.
will get trouble because *mekano* won't know which file is generated and which
is not.
Known limitations

@@ -371,3 +397,4 @@ ----------------

* a bug appears, for the command `watch' only, when a
[file is renamed](https://github.com/shama/gaze/issues/107).
[file is renamed](https://github.com/shama/gaze/issues/107);
* generative transformations accept only one pattern prerequisite.

@@ -384,3 +411,5 @@ See also the [ROADMAP](./ROADMAP.md).

* directories are handled automatically;
* it detects command line changes.
* it detects command line changes;
* it can watch files out-of-the-box;
* it also runs concurrently (GNU Make's `-j` option).

@@ -391,8 +420,10 @@ ### Why using this instead of grunt?

* no plugin system, you can use tools from any package, in any version; 'less
is more' applies pretty well to this case.
is more' applies pretty well to this case;
* concurrency.
### Why *not* using mekano?
* it's still in beta and may be unstable;
* too high-level, you have specific dependency needs;
* no logic, no 'if', or too limited semantics;
* no logic, no 'if', limited semantics;
* might be too slow for medium or large projects.

@@ -407,3 +438,3 @@

* inference is done the other way around than *make* (it infers targets based
on prerequisites; make does the contrary with rules like `%.o: %c`).
on prerequisites; make does the opposite with rules like `%.o: %c`).

@@ -415,4 +446,4 @@ ### Shout out

* the historic [GNU make](http://www.gnu.org/software/make/manual/make.html);
* the super-fast [Ninja](http://martine.github.io/ninja/);
* the insightful [tup](http://gittup.org/tup/);
* the pragmatic [grunt](http://gruntjs.com/).
* the fast [Ninja](http://martine.github.io/ninja/);
* the cutting-edge [tup](http://gittup.org/tup/);
* the down-to-earth [grunt](http://gruntjs.com/).
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc