Socket
Socket
Sign inDemoInstall

gaze

Package Overview
Dependencies
Maintainers
1
Versions
32
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

gaze - npm Package Compare versions

Comparing version 0.5.1 to 0.6.0

AUTHORS

450

lib/gaze.js

@@ -5,3 +5,3 @@ /*

*
* Copyright (c) 2013 Kyle Robinson Young
* Copyright (c) 2014 Kyle Robinson Young
* Licensed under the MIT license.

@@ -18,13 +18,9 @@ */

var globule = require('globule');
var nextback = require('nextback');
var helper = require('./helper');
var platform = require('./platform');
// shim setImmediate for node v0.8
var setImmediate = require('timers').setImmediate;
if (typeof setImmediate !== 'function') {
setImmediate = process.nextTick;
}
// keep track of instances to call multiple times for backwards compatibility
var instances = [];
// globals
var delay = 10;
// `Gaze` EventEmitter object to return in the callback

@@ -35,3 +31,8 @@ function Gaze(patterns, opts, done) {

// If second arg is the callback
// Optional arguments
if (typeof patterns === 'function') {
done = patterns;
patterns = null;
opts = {};
}
if (typeof opts === 'function') {

@@ -45,3 +46,3 @@ done = opts;

opts.mark = true;
opts.interval = opts.interval || 100;
opts.interval = opts.interval || 500;
opts.debounceDelay = opts.debounceDelay || 500;

@@ -51,2 +52,15 @@ opts.cwd = opts.cwd || process.cwd();

// Default error handler to prevent emit('error') throwing magically for us
this.on('error', function(error) {
if (self.listeners('error').length > 1) {
return self.removeListener('error', this);
}
nextback(function() {
done.call(self, error, self);
})();
});
// File watching mode to use when adding files to the platform
this._mode = opts.mode || 'auto';
// Default done callback

@@ -83,4 +97,13 @@ done = done || function() {};

// keep the process alive
this._keepalive = setInterval(function() {}, 200);
this._keepalive = setInterval(platform.tick.bind(platform), opts.interval);
// Keep track of all instances created
this._instanceNum = instances.length;
instances.push(this);
// Keep track of safewriting and debounce timeouts
this._safewriting = null;
this._safewriteTimeout = null;
this._timeoutId = null;
return this;

@@ -104,3 +127,2 @@ }

var filepath = args[1];
var timeoutId;

@@ -125,2 +147,22 @@ // If not added/deleted/changed/renamed then just emit the event

// Detect safewrite events, if file is deleted and then added/renamed, assume a safewrite happened
if (e === 'deleted' && this._safewriting == null) {
this._safewriting = filepath;
this._safewriteTimeout = setTimeout(function() {
// Just a normal delete, carry on
Gaze.super_.prototype.emit.apply(self, args);
Gaze.super_.prototype.emit.apply(self, ['all', e].concat([].slice.call(args, 1)));
self._safewriting = null;
}, this.options.debounceDelay);
return this;
} else if ((e === 'added' || e === 'renamed') && this._safewriting === filepath) {
clearTimeout(this._safewriteTimeout);
this._safewriteTimeout = setTimeout(function() {
self._safewriting = null;
}, this.options.debounceDelay);
args[0] = e = 'changed';
} else if (e === 'deleted' && this._safewriting === filepath) {
return this;
}
// If cached doesnt exist, create a delay before running the next

@@ -131,4 +173,4 @@ // then emit the event

helper.objectPush(self._cached, filepath, e);
clearTimeout(timeoutId);
timeoutId = setTimeout(function() {
clearTimeout(this._timeoutId);
this._timeoutId = setTimeout(function() {
delete self._cached[filepath];

@@ -159,20 +201,5 @@ }, this.options.debounceDelay);

Gaze.prototype.close = function(_reset) {
var self = this;
_reset = _reset === false ? false : true;
Object.keys(self._watchers).forEach(function(file) {
self._watchers[file].close();
});
self._watchers = Object.create(null);
Object.keys(this._watched).forEach(function(dir) {
self._unpollDir(dir);
});
if (_reset) {
self._watched = Object.create(null);
setTimeout(function() {
self.emit('end');
self.removeAllListeners();
clearInterval(self._keepalive);
}, delay + 100);
}
return self;
instances.splice(this._instanceNum, 1);
platform.closeAll();
this.emit('end');
};

@@ -182,269 +209,154 @@

Gaze.prototype.add = function(files, done) {
var self = this;
if (typeof files === 'string') { files = [files]; }
this._patterns = helper.unique.apply(null, [this._patterns, files]);
files = globule.find(this._patterns, this.options);
this._addToWatched(files);
this.close(false);
this._initWatched(done);
// If no matching files
if (files.length < 1) {
// Defer to emitting to give a chance to attach event handlers.
nextback(function() {
self.emit('ready', self);
if (done) done.call(self, null, self);
self.emit('nomatch');
})();
return;
}
// Set the platform mode before adding files
platform.mode = self._mode;
helper.forEachSeries(files, function(file, next) {
try {
platform(path.join(self.options.cwd, file), self._trigger.bind(self));
} catch (err) {
self.emit('error', err);
}
next();
}, function() {
// A little delay here for backwards compatibility, lol
setTimeout(function() {
self.emit('ready', self);
if (done) done.call(self, null, self);
}, 10);
});
};
// Dont increment patterns and dont call done if nothing added
Gaze.prototype._internalAdd = function(file, done) {
var files = [];
if (helper.isDir(file)) {
files = [helper.markDir(file)].concat(globule.find(this._patterns, this.options));
} else {
if (globule.isMatch(this._patterns, file, this.options)) {
files = [file];
}
// Call when the platform has triggered
Gaze.prototype._trigger = function(error, event, filepath, newFile) {
if (error) { return this.emit('error', error); }
// Set the platform mode before adding files
platform.mode = this._mode;
if (event === 'change' && helper.isDir(filepath)) {
this._wasAdded(filepath);
} else if (event === 'change') {
this._emitAll('changed', filepath);
} else if (event === 'delete') {
// Close out deleted filepaths (important to make safewrite detection work)
platform.close(filepath);
this._emitAll('deleted', filepath);
} else if (event === 'rename') {
// TODO: This occasionally throws, figure out why or use the old style rename detect
// The handle(26) returned by watching [filename] is the same with an already watched path([filename])
this._emitAll('renamed', newFile, filepath);
}
if (files.length > 0) {
this._addToWatched(files);
this.close(false);
this._initWatched(done);
}
};
// Remove file/dir from `watched`
Gaze.prototype.remove = function(file) {
// If a folder received a change event, investigate
Gaze.prototype._wasAdded = function(dir) {
var self = this;
if (this._watched[file]) {
// is dir, remove all files
this._unpollDir(file);
delete this._watched[file];
} else {
// is a file, find and remove
Object.keys(this._watched).forEach(function(dir) {
var index = self._watched[dir].indexOf(file);
if (index !== -1) {
self._unpollFile(file);
self._watched[dir].splice(index, 1);
return false;
var dirstat = fs.statSync(dir);
fs.readdir(dir, function(err, current) {
helper.forEachSeries(current, function(file, next) {
var filepath = path.join(dir, file);
if (!fs.existsSync(filepath)) return next();
var stat = fs.lstatSync(filepath);
if ((dirstat.mtime - stat.mtime) <= 0) {
var relpath = path.relative(self.options.cwd, filepath);
if (stat.isDirectory()) {
// If it was a dir, watch the dir and emit that it was added
platform(filepath, self._trigger.bind(self));
self._emitAll('added', filepath);
self._wasAddedSub(filepath);
} else if (globule.isMatch(self._patterns, relpath, self.options)) {
// Otherwise if the file matches, emit added
platform(filepath, self._trigger.bind(self));
self._emitAll('added', filepath);
}
}
next();
});
}
if (this._watchers[file]) {
this._watchers[file].close();
}
return this;
});
};
// Return watched files
Gaze.prototype.watched = function() {
return this._watched;
};
// Returns `watched` files with relative paths to process.cwd()
Gaze.prototype.relative = function(dir, unixify) {
// If a sub folder was added, investigate further
// Such as with grunt.file.write('new_dir/tmp.js', '') as it will create the folder and file simultaneously
Gaze.prototype._wasAddedSub = function(dir) {
var self = this;
var relative = Object.create(null);
var relDir, relFile, unixRelDir;
var cwd = this.options.cwd || process.cwd();
if (dir === '') { dir = '.'; }
dir = helper.markDir(dir);
unixify = unixify || false;
Object.keys(this._watched).forEach(function(dir) {
relDir = path.relative(cwd, dir) + path.sep;
if (relDir === path.sep) { relDir = '.'; }
unixRelDir = unixify ? helper.unixifyPathSep(relDir) : relDir;
relative[unixRelDir] = self._watched[dir].map(function(file) {
relFile = path.relative(path.join(cwd, relDir) || '', file || '');
if (helper.isDir(file)) {
relFile = helper.markDir(relFile);
fs.readdir(dir, function(err, current) {
helper.forEachSeries(current, function(file, next) {
var filepath = path.join(dir, file);
var relpath = path.relative(self.options.cwd, filepath);
try {
if (fs.lstatSync(filepath).isDirectory()) {
self._wasAdded(filepath);
} else if (globule.isMatch(self._patterns, relpath, self.options)) {
// Make sure to watch the newly added sub file
platform(filepath, self._trigger.bind(self));
self._emitAll('added', filepath);
}
} catch (err) {
self.emit('error', err);
}
if (unixify) {
relFile = helper.unixifyPathSep(relFile);
}
return relFile;
next();
});
});
if (dir && unixify) {
dir = helper.unixifyPathSep(dir);
}
return dir ? relative[dir] || [] : relative;
};
// Adds files and dirs to watched
Gaze.prototype._addToWatched = function(files) {
for (var i = 0; i < files.length; i++) {
var file = files[i];
var filepath = path.resolve(this.options.cwd, file);
var dirname = (helper.isDir(file)) ? filepath : path.dirname(filepath);
dirname = helper.markDir(dirname);
// If a new dir is added
if (helper.isDir(file) && !(filepath in this._watched)) {
helper.objectPush(this._watched, filepath, []);
}
if (file.slice(-1) === '/') { filepath += path.sep; }
helper.objectPush(this._watched, path.dirname(filepath) + path.sep, filepath);
// add folders into the mix
var readdir = fs.readdirSync(dirname);
for (var j = 0; j < readdir.length; j++) {
var dirfile = path.join(dirname, readdir[j]);
if (fs.statSync(dirfile).isDirectory()) {
helper.objectPush(this._watched, dirname, dirfile + path.sep);
}
}
// Wrapper for emit to ensure we emit on all instances
Gaze.prototype._emitAll = function() {
var args = Array.prototype.slice.call(arguments);
for (var i = 0; i < instances.length; i++) {
instances[i].emit.apply(instances[i], args);
}
return this;
};
Gaze.prototype._watchDir = function(dir, done) {
var self = this;
var timeoutId;
try {
this._watchers[dir] = fs.watch(dir, function(event) {
// race condition. Let's give the fs a little time to settle down. so we
// don't fire events on non existent files.
clearTimeout(timeoutId);
timeoutId = setTimeout(function() {
// race condition. Ensure that this directory is still being watched
// before continuing.
if ((dir in self._watchers) && fs.existsSync(dir)) {
done(null, dir);
}
}, delay + 100);
});
} catch (err) {
return this._handleError(err);
}
// Remove file/dir from `watched`
Gaze.prototype.remove = function(file) {
platform.close(file);
return this;
};
Gaze.prototype._unpollFile = function(file) {
if (this._pollers[file]) {
fs.unwatchFile(file, this._pollers[file] );
delete this._pollers[file];
}
// Return watched files
Gaze.prototype.watched = function(done) {
done = nextback(done || function() {});
helper.flatToTree(platform.getWatchedPaths(), this.options.cwd, false, done);
return this;
};
Gaze.prototype._unpollDir = function(dir) {
this._unpollFile(dir);
for (var i = 0; i < this._watched[dir].length; i++) {
this._unpollFile(this._watched[dir][i]);
// Returns `watched` files with relative paths to cwd
Gaze.prototype.relative = function(dir, unixify, done) {
if (typeof dir === 'function') {
done = dir;
dir = null;
unixify = false;
}
};
Gaze.prototype._pollFile = function(file, done) {
var opts = { persistent: true, interval: this.options.interval };
if (!this._pollers[file]) {
this._pollers[file] = function(curr, prev) {
done(null, file);
};
try {
fs.watchFile(file, opts, this._pollers[file]);
} catch (err) {
return this._handleError(err);
if (typeof unixify === 'function') {
done = unixify;
unixify = false;
}
done = nextback(done || function() {});
helper.flatToTree(platform.getWatchedPaths(), this.options.cwd, true, unixify, function(err, relative) {
if (dir) {
if (unixify) {
dir = helper.unixifyPathSep(dir);
}
// Better guess what to return for backwards compatibility
return done(null, relative[dir] || relative[dir + (unixify ? '/' : path.sep)] || []);
}
}
return done(null, relative);
});
return this;
};
// Initialize the actual watch on `watched` files
Gaze.prototype._initWatched = function(done) {
var self = this;
var cwd = this.options.cwd || process.cwd();
var curWatched = Object.keys(self._watched);
// if no matching files
if (curWatched.length < 1) {
// Defer to emitting to give a chance to attach event handlers.
setImmediate(function () {
self.emit('ready', self);
if (done) { done.call(self, null, self); }
self.emit('nomatch');
});
return;
}
helper.forEachSeries(curWatched, function(dir, next) {
dir = dir || '';
var files = self._watched[dir];
// Triggered when a watched dir has an event
self._watchDir(dir, function(event, dirpath) {
var relDir = cwd === dir ? '.' : path.relative(cwd, dir);
relDir = relDir || '';
fs.readdir(dirpath, function(err, current) {
if (err) { return self.emit('error', err); }
if (!current) { return; }
try {
// append path.sep to directories so they match previous.
current = current.map(function(curPath) {
if (fs.existsSync(path.join(dir, curPath)) && fs.statSync(path.join(dir, curPath)).isDirectory()) {
return curPath + path.sep;
} else {
return curPath;
}
});
} catch (err) {
// race condition-- sometimes the file no longer exists
}
// Get watched files for this dir
var previous = self.relative(relDir);
// If file was deleted
previous.filter(function(file) {
return current.indexOf(file) < 0;
}).forEach(function(file) {
if (!helper.isDir(file)) {
var filepath = path.join(dir, file);
self.remove(filepath);
self.emit('deleted', filepath);
}
});
// If file was added
current.filter(function(file) {
return previous.indexOf(file) < 0;
}).forEach(function(file) {
// Is it a matching pattern?
var relFile = path.join(relDir, file);
// Add to watch then emit event
self._internalAdd(relFile, function() {
self.emit('added', path.join(dir, file));
});
});
});
});
// Watch for change/rename events on files
files.forEach(function(file) {
if (helper.isDir(file)) { return; }
self._pollFile(file, function(err, filepath) {
// Only emit changed if the file still exists
// Prevents changed/deleted duplicate events
if (fs.existsSync(filepath)) {
self.emit('changed', filepath);
}
});
});
next();
}, function() {
// Return this instance of Gaze
// delay before ready solves a lot of issues
setTimeout(function() {
self.emit('ready', self);
if (done) { done.call(self, null, self); }
}, delay + 100);
});
};
// If an error, handle it here
Gaze.prototype._handleError = function(err) {
if (err.code === 'EMFILE') {
return this.emit('error', new Error('EMFILE: Too many opened files.'));
}
return this.emit('error', err);
};

@@ -0,1 +1,9 @@

/*
* gaze
* https://github.com/shama/gaze
*
* Copyright (c) 2014 Kyle Robinson Young
* Licensed under the MIT license.
*/
'use strict';

@@ -9,3 +17,3 @@

if (typeof dir !== 'string') { return false; }
return (dir.slice(-(path.sep.length)) === path.sep);
return (dir.slice(-(path.sep.length)) === path.sep) || (dir.slice(-1) === '/');
};

@@ -36,2 +44,45 @@

// Converts a flat list of paths to the old style tree
helper.flatToTree = function flatToTree(files, cwd, relative, unixify, done) {
cwd = helper.markDir(cwd);
var tree = Object.create(null);
helper.forEachSeries(files, function(filepath, next) {
var parent = path.dirname(filepath) + path.sep;
// If parent outside cwd, ignore
if (path.relative(cwd, parent) === '..') {
return next();
}
// If we want relative paths
if (relative === true) {
if (path.resolve(parent) === path.resolve(cwd)) {
parent = './';
} else {
parent = path.relative(cwd, parent) + path.sep;
}
filepath = path.relative(path.join(cwd, parent), filepath) + (helper.isDir(filepath) ? path.sep : '');
}
// If we want to transform paths to unix seps
if (unixify === true) {
filepath = helper.unixifyPathSep(filepath);
if (parent !== './') {
parent = helper.unixifyPathSep(parent);
}
}
if (!parent) { return next(); }
if (!Array.isArray(tree[parent])) {
tree[parent] = [];
}
tree[parent].push(filepath);
next();
}, function() {
done(null, tree);
});
};
/**

@@ -51,12 +102,16 @@ * Lo-Dash 1.0.1 <http://lodash.com/>

helper.forEachSeries = function forEachSeries(arr, iterator, callback) {
if (!arr.length) { return callback(); }
callback = callback || function () {};
if (!arr.length) {
return callback();
}
var completed = 0;
var iterate = function() {
var iterate = function () {
iterator(arr[completed], function (err) {
if (err) {
callback(err);
callback = function() {};
} else {
callback = function () {};
}
else {
completed += 1;
if (completed === arr.length) {
if (completed >= arr.length) {
callback(null);

@@ -63,0 +118,0 @@ } else {

{
"name": "gaze",
"description": "A globbing fs.watch wrapper built from the best parts of other fine watch libs.",
"version": "0.5.1",
"version": "0.6.0",
"homepage": "https://github.com/shama/gaze",

@@ -23,3 +23,3 @@ "author": {

],
"main": "lib/gaze",
"main": "index.js",
"engines": {

@@ -29,24 +29,38 @@ "node": ">= 0.8.0"

"scripts": {
"test": "grunt nodeunit -v"
"test": "grunt nodeunit -v",
"install": "node-gyp rebuild"
},
"dependencies": {
"globule": "~0.1.0"
"globule": "~0.2.0",
"nextback": "~0.1.0",
"bindings": "~1.2.0",
"nan": "~0.8.0"
},
"devDependencies": {
"grunt": "~0.4.1",
"grunt-contrib-nodeunit": "~0.2.0",
"grunt-contrib-jshint": "~0.6.0",
"grunt-benchmark": "~0.2.0",
"grunt-contrib-nodeunit": "~0.3.3",
"grunt-contrib-jshint": "~0.9.2",
"grunt-cli": "~0.1.13",
"async": "~0.2.10",
"rimraf": "~2.2.6"
"rimraf": "~2.2.6",
"ascii-table": "0.0.4"
},
"keywords": [
"watch",
"glob"
"watcher",
"watching",
"fs.watch",
"fswatcher",
"fs",
"glob",
"utility"
],
"files": [
"index.js",
"lib",
"src",
"binding.gyp",
"AUTHORS",
"LICENSE-MIT"
]
}

@@ -1,2 +0,2 @@

# gaze [![Build Status](https://travis-ci.org/shama/gaze.png?branch=master)](https://travis-ci.org/shama/gaze)
# gaze [![Build Status](http://img.shields.io/travis/shama/gaze.svg)](https://travis-ci.org/shama/gaze) [![gittip.com/shama](http://img.shields.io/gittip/shama.svg)](https://www.gittip.com/shama)

@@ -8,2 +8,14 @@ A globbing fs.watch wrapper built from the best parts of other fine watch libs.

## Features
[![NPM](https://nodei.co/npm/gaze.png?downloads=true)](https://nodei.co/npm/gaze/)
* Consistent events on OSX, Linux and Windows
* Very fast start up and response time
* High test coverage
* Uses native OS events but falls back to stat polling
* Option to force stat polling with special file systems such as networked
* Downloaded over 400K times a month
* Used by [Grunt](http://gruntjs.com), [gulp](http://gulpjs.com), [Tower](http://tower.github.io/) and many others
## Usage

@@ -22,3 +34,5 @@ Install the module with: `npm install gaze` or place into your `package.json`

// Get all watched files
console.log(this.watched());
this.watched(function(watched) {
console.log(watched);
});

@@ -46,3 +60,5 @@ // On file changed

// Get watched files with relative paths
console.log(this.relative());
this.relative(function(err, files) {
console.log(files);
});
});

@@ -74,9 +90,24 @@

```javascript
gaze('**/*', function() {
this.on('error', function(err) {
// Handle error here
});
gaze('**/*', function(error, watcher) {
if (error) {
// Handle error if it occured while starting up
}
});
// Or with the alternative interface
var gaze = new Gaze();
gaze.on('error', function(error) {
// Handle error here
});
gaze.add('**/*');
```
#### `EMFILE` errors
By default, gaze will use native OS events and then fallback to slower stat polling when an `EMFILE` error is reached. Gaze will still emit or return the error as the first argument of the ready callback for you to handle.
It is recommended to advise your users to increase their file descriptor limits to utilize the faster native OS watching. Especially on OSX where the default descriptor limit is 256.
In some cases, native OS events will not work. Such as with networked file systems or vagrant. It is recommended to set the option `mode: 'poll'` to always stat poll for those situations.
### Minimatch / Glob

@@ -89,3 +120,3 @@

### gaze(patterns, [options], callback)
### gaze([patterns, options, callback])

@@ -100,3 +131,3 @@ * `patterns` {String|Array} File patterns to be matched

Create a Gaze object by instanting the `gaze.Gaze` class.
Create a Gaze object by instancing the `gaze.Gaze` class.

@@ -114,2 +145,4 @@ ```javascript

file/event
* `mode` {string} Force the watch mode. Either `'auto'` (default), `'watch'` (force native events), or `'poll'` (force stat polling).
* `cwd` {string} The current working directory to base file patterns from. Default is `process.cwd()`.

@@ -136,6 +169,8 @@ #### Events

recurse directories.
* `watched()` Returns the currently watched files.
* `relative([dir, unixify])` Returns the currently watched files with relative paths.
* `watched([callback])` Returns the currently watched files.
* `callback` {function} Calls with `function(err, files)`.
* `relative([dir, unixify, callback])` Returns the currently watched files with relative paths.
* `dir` {string} Only return relative files for this directory.
* `unixify` {boolean} Return paths with `/` instead of `\\` if on Windows.
* `callback` {function} Calls with `function(err, files)`.

@@ -146,14 +181,12 @@ ## FAQs

I liked parts of other `fs.watch` wrappers but none had all the features I
needed. This lib combines the features I needed from other fine watch libs:
Speedy data behavior from
[paulmillr's chokidar](https://github.com/paulmillr/chokidar), API interface
from [mikeal's watch](https://github.com/mikeal/watch) and file globbing using
[isaacs's glob](https://github.com/isaacs/node-glob) which is also used by
[cowboy's Grunt](https://github.com/gruntjs/grunt).
needed when this library was originally written. This lib once combined the features I needed from other fine watch libs
but now has taken on a life of it's own (**gaze doesn't wrap `fs.watch` or `fs.watchFile` anymore**).
### How do I fix the error `EMFILE: Too many opened files.`?
This is because of your system's max opened file limit. For OSX the default is
very low (256). Increase your limit temporarily with `ulimit -n 10480`, the
number being the new max limit.
Other great watch libraries to try are:
* [paulmillr's chokidar](https://github.com/paulmillr/chokidar)
* [mikeal's watch](https://github.com/mikeal/watch)
* [github's pathwatcher](https://github.com/atom/node-pathwatcher)
* [bevry's watchr](https://github.com/bevry/watchr)
## Contributing

@@ -165,2 +198,3 @@ In lieu of a formal styleguide, take care to maintain the existing coding style.

## Release History
* 0.6.0 - Uses native OS events (fork of pathwatcher) but can fall back to stat polling. Everything is async to avoid blocking, including `relative()` and `watched()`. Better error handling. Update to globule@0.2.0. No longer watches `cwd` by default. Added `mode` option. Better `EMFILE` message. Avoids `ENOENT` errors with symlinks. All constructor arguments are optional.
* 0.5.1 - Use setImmediate (process.nextTick for node v0.8) to defer ready/nomatch events (@amasad).

@@ -189,3 +223,3 @@ * 0.5.0 - Process is now kept alive while watching files. Emits a nomatch event when no files are matching.

## License
Copyright (c) 2013 Kyle Robinson Young
Copyright (c) 2014 Kyle Robinson Young
Licensed under the MIT license.

Sorry, the diff of this file is not supported yet

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