npm install node-static
authorJack Allnutt <m2ys4u@Gmail.com>
Tue, 19 Jul 2011 01:56:58 +0000 (02:56 +0100)
committerJack Allnutt <m2ys4u@Gmail.com>
Tue, 19 Jul 2011 01:56:58 +0000 (02:56 +0100)
node/node_modules/node-static/LICENSE [new file with mode: 0644]
node/node_modules/node-static/README.md [new file with mode: 0644]
node/node_modules/node-static/benchmark/node-static-0.3.0.txt [new file with mode: 0644]
node/node_modules/node-static/examples/file-server.js [new file with mode: 0644]
node/node_modules/node-static/lib/node-static.js [new file with mode: 0644]
node/node_modules/node-static/lib/node-static/mime.js [new file with mode: 0644]
node/node_modules/node-static/lib/node-static/util.js [new file with mode: 0644]
node/node_modules/node-static/package.json [new file with mode: 0644]

diff --git a/node/node_modules/node-static/LICENSE b/node/node_modules/node-static/LICENSE
new file mode 100644 (file)
index 0000000..91f6632
--- /dev/null
@@ -0,0 +1,20 @@
+Copyright (c) 2010 Alexis Sellier
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/node/node_modules/node-static/README.md b/node/node_modules/node-static/README.md
new file mode 100644 (file)
index 0000000..825c811
--- /dev/null
@@ -0,0 +1,141 @@
+node-static
+===========
+
+> a simple, *rfc 2616 compliant* file streaming module for [node](http://nodejs.org)
+
+node-static has an in-memory file cache, making it highly efficient.
+node-static understands and supports *conditional GET* and *HEAD* requests.
+node-static was inspired by some of the other static-file serving modules out there,
+such as node-paperboy and antinode.
+
+synopsis
+--------
+
+    var static = require('node-static');
+
+    //
+    // Create a node-static server instance to serve the './public' folder
+    //
+    var file = new(static.Server)('./public');
+
+    require('http').createServer(function (request, response) {
+        request.addListener('end', function () {
+            //
+            // Serve files!
+            //
+            file.serve(request, response);
+        });
+    }).listen(8080);
+
+API
+---
+
+### Creating a node-static Server #
+
+Creating a file server instance is as simple as:
+
+    new static.Server();
+
+This will serve files in the current directory. If you want to serve files in a specific
+directory, pass it as the first argument:
+
+    new static.Server('./public');
+
+You can also specify how long the client is supposed to cache the files node-static serves:
+
+    new static.Server('./public', { cache: 3600 });
+
+This will set the `Cache-Control` header, telling clients to cache the file for an hour.
+This is the default setting.
+
+### Serving files under a directory #
+
+To serve files under a directory, simply call the `serve` method on a `Server` instance, passing it
+the HTTP request and response object:
+
+    var fileServer = new static.Server('./public');
+
+    require('http').createServer(function (request, response) {
+        request.addListener('end', function () {
+            fileServer.serve(request, response);
+        });
+    }).listen(8080);
+
+### Serving specific files #
+
+If you want to serve a specific file, like an error page for example, use the `serveFile` method:
+
+    fileServer.serveFile('/error.html', 500, {}, request, response);
+
+This will serve the `error.html` file, from under the file root directory, with a `500` status code.
+For example, you could serve an error page, when the initial request wasn't found:
+
+    require('http').createServer(function (request, response) {
+        request.addListener('end', function () {
+            fileServer.serve(request, response, function (e, res) {
+                if (e && (e.status === 404)) { // If the file wasn't found
+                    fileServer.serveFile('/not-found.html', request, response);
+                }
+            });
+        });
+    }).listen(8080);
+
+More on intercepting errors bellow.
+
+### Intercepting errors & Listening #
+
+An optional callback can be passed as last argument, it will be called every time a file
+has been served successfully, or if there was an error serving the file:
+
+    var fileServer = new static.Server('./public');
+
+    require('http').createServer(function (request, response) {
+        request.addListener('end', function () {
+            fileServer.serve(request, response, function (err, result) {
+                if (err) { // There was an error serving the file
+                    sys.error("Error serving " + request.url + " - " + err.message);
+
+                    // Respond to the client
+                    response.writeHead(err.status, err.headers);
+                    response.end();
+                }
+            });
+        });
+    }).listen(8080);
+
+Note that if you pass a callback, and there is an error serving the file, node-static
+*will not* respond to the client. This gives you the opportunity to re-route the request,
+or handle it differently.
+
+For example, you may want to interpret a request as a static request, but if the file isn't found,
+send it to an application.
+
+If you only want to *listen* for errors, you can use *event listeners*:
+
+    fileServer.serve(request, response).addListener('error', function (err) {
+        sys.error("Error serving " + request.url + " - " + err.message);
+    });
+
+With this method, you don't have to explicitly send the response back, in case of an error.
+
+### Options when creating an instance of `Server` #
+
+#### `cache` #
+
+Sets the `Cache-Control` header.
+
+example: `{ cache: 7200 }`
+
+Passing a number will set the cache duration to that number of seconds.
+Passing `false` will disable the `Cache-Control` header.
+
+> Defaults to `3600`
+
+#### `headers` #
+
+Sets response headers.
+
+example: `{ 'X-Hello': 'World!' }`
+
+> defaults to `{}`
+
diff --git a/node/node_modules/node-static/benchmark/node-static-0.3.0.txt b/node/node_modules/node-static/benchmark/node-static-0.3.0.txt
new file mode 100644 (file)
index 0000000..c6083ea
--- /dev/null
@@ -0,0 +1,43 @@
+This is ApacheBench, Version 2.3 <$Revision: 655654 $>
+Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
+Licensed to The Apache Software Foundation, http://www.apache.org/
+
+Benchmarking 127.0.0.1 (be patient)
+
+
+Server Software:        node-static/0.3.0
+Server Hostname:        127.0.0.1
+Server Port:            8080
+
+Document Path:          /lib/node-static.js
+Document Length:        6038 bytes
+
+Concurrency Level:      20
+Time taken for tests:   2.323 seconds
+Complete requests:      10000
+Failed requests:        0
+Write errors:           0
+Total transferred:      63190000 bytes
+HTML transferred:       60380000 bytes
+Requests per second:    4304.67 [#/sec] (mean)
+Time per request:       4.646 [ms] (mean)
+Time per request:       0.232 [ms] (mean, across all concurrent requests)
+Transfer rate:          26563.66 [Kbytes/sec] received
+
+Connection Times (ms)
+              min  mean[+/-sd] median   max
+Connect:        0    0   0.2      0       3
+Processing:     1    4   1.4      4      28
+Waiting:        1    4   1.3      4      18
+Total:          2    5   1.5      4      28
+
+Percentage of the requests served within a certain time (ms)
+  50%      4
+  66%      5
+  75%      5
+  80%      5
+  90%      5
+  95%      6
+  98%      8
+  99%      9
+ 100%     28 (longest request)
diff --git a/node/node_modules/node-static/examples/file-server.js b/node/node_modules/node-static/examples/file-server.js
new file mode 100644 (file)
index 0000000..a12c43b
--- /dev/null
@@ -0,0 +1,26 @@
+var sys = require('sys');
+var static = require('../lib/node-static');
+
+//
+// Create a node-static server to serve the current directory
+//
+var file = new(static.Server)('.', { cache: 7200, headers: {'X-Hello':'World!'} });
+
+require('http').createServer(function (request, response) {
+    request.addListener('end', function () {
+        //
+        // Serve files!
+        //
+        file.serve(request, response, function (err, res) {
+            if (err) { // An error as occured
+                sys.error("> Error serving " + request.url + " - " + err.message);
+                response.writeHead(err.status, err.headers);
+                response.end();
+            } else { // The file was served successfully
+                sys.puts("> " + request.url + " - " + res.message);
+            }
+        });
+    });
+}).listen(8080);
+
+sys.puts("> node-static is listening on http://127.0.0.1:8080");
diff --git a/node/node_modules/node-static/lib/node-static.js b/node/node_modules/node-static/lib/node-static.js
new file mode 100644 (file)
index 0000000..ef8ad5a
--- /dev/null
@@ -0,0 +1,252 @@
+var fs = require('fs'),
+    sys = require('sys'),
+    events = require('events'),
+    buffer = require('buffer'),
+    http = require('http'),
+    url = require('url'),
+    path = require('path');
+
+this.version = [0, 5, 6];
+
+var mime = require('./node-static/mime');
+var util = require('./node-static/util');
+
+var serverInfo = 'node-static/' + this.version.join('.');
+
+// In-memory file store
+this.store = {};
+this.indexStore = {};
+
+this.Server = function (root, options) {
+    if (root && (typeof(root) === 'object')) { options = root, root = null }
+
+    this.root    = path.normalize(root || '.');
+    this.options = options || {};
+    this.cache   = 3600;
+
+    this.defaultHeaders  = {};
+    this.options.headers = this.options.headers || {};
+
+    if ('cache' in this.options) {
+        if (typeof(this.options.cache) === 'number') {
+            this.cache = this.options.cache;
+        } else if (! this.options.cache) {
+            this.cache = false;
+        }
+    }
+
+    if (this.cache !== false) {
+        this.defaultHeaders['Cache-Control'] = 'max-age=' + this.cache;
+    }
+    this.defaultHeaders['Server'] = serverInfo;
+
+    for (var k in this.defaultHeaders) {
+        this.options.headers[k] = this.options.headers[k] ||
+                                  this.defaultHeaders[k];
+    }
+};
+
+this.Server.prototype.serveDir = function (pathname, req, res, finish) {
+    var htmlIndex = path.join(pathname, 'index.html'),
+        that = this;
+
+    fs.stat(htmlIndex, function (e, stat) {
+        if (!e) {
+            that.respond(null, 200, {}, [htmlIndex], stat, req, res, finish);
+        } else {
+            if (pathname in exports.store) {
+                streamFiles(exports.indexStore[pathname].files);
+            } else {
+                // Stream a directory of files as a single file.
+                fs.readFile(path.join(pathname, 'index.json'), function (e, contents) {
+                    if (e) { return finish(404, {}) }
+                    var index = JSON.parse(contents);
+                    exports.indexStore[pathname] = index;
+                    streamFiles(index.files);
+                });
+            }
+        }
+    });
+    function streamFiles(files) {
+        util.mstat(pathname, files, function (e, stat) {
+            that.respond(pathname, 200, {}, files, stat, req, res, finish);
+        });
+    }
+};
+this.Server.prototype.serveFile = function (pathname, status, headers, req, res) {
+    var that = this;
+    var promise = new(events.EventEmitter);
+
+    pathname = this.normalize(pathname);
+
+    fs.stat(pathname, function (e, stat) {
+        if (e) {
+            return promise.emit('error', e);
+        }
+        that.respond(null, status, headers, [pathname], stat, req, res, function (status, headers) {
+            that.finish(status, headers, req, res, promise);
+        });
+    });
+    return promise;
+};
+this.Server.prototype.finish = function (status, headers, req, res, promise, callback) {
+    var result = {
+        status:  status,
+        headers: headers,
+        message: http.STATUS_CODES[status]
+    };
+
+    headers['Server'] = serverInfo;
+
+    if (!status || status >= 400) {
+        if (callback) {
+            callback(result);
+        } else {
+            if (promise.listeners('error').length > 0) {
+                promise.emit('error', result);
+            }
+            res.writeHead(status, headers);
+            res.end();
+        }
+    } else {
+        // Don't end the request here, if we're streaming;
+        // it's taken care of in `prototype.stream`.
+        if (status !== 200 || req.method !== 'GET') {
+            res.writeHead(status, headers);
+            res.end();
+        }
+        callback && callback(null, result);
+        promise.emit('success', result);
+    }
+};
+
+this.Server.prototype.servePath = function (pathname, status, headers, req, res, finish) {
+    var that = this,
+        promise = new(events.EventEmitter);
+
+    pathname = this.normalize(pathname);
+
+    // Only allow GET and HEAD requests
+    if (req.method !== 'GET' && req.method !== 'HEAD') {
+        finish(405, { 'Allow': 'GET, HEAD' });
+        return promise;
+    }
+
+    // Make sure we're not trying to access a
+    // file outside of the root.
+    if (new(RegExp)('^' + that.root).test(pathname)) {
+        fs.stat(pathname, function (e, stat) {
+            if (e) {
+                finish(404, {});
+            } else if (stat.isFile()) {      // Stream a single file.
+                that.respond(null, status, headers, [pathname], stat, req, res, finish);
+            } else if (stat.isDirectory()) { // Stream a directory of files.
+                that.serveDir(pathname, req, res, finish);
+            } else {
+                finish(400, {});
+            }
+        });
+    } else {
+        // Forbidden
+        finish(403, {});
+    }
+    return promise;
+};
+this.Server.prototype.normalize = function (pathname) {
+    return path.normalize(path.join(this.root, pathname));
+};
+this.Server.prototype.serve = function (req, res, callback) {
+    var that = this,
+        promise = new(events.EventEmitter);
+
+    var pathname = url.parse(req.url).pathname;
+
+    var finish = function (status, headers) {
+        that.finish(status, headers, req, res, promise, callback);
+    };
+
+    process.nextTick(function () {
+        that.servePath(pathname, 200, {}, req, res, finish).on('success', function (result) {
+            promise.emit('success', result);
+        }).on('error', function (err) {
+            promise.emit('error');
+        });
+    });
+    if (! callback) { return promise }
+};
+
+this.Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) {
+    var mtime   = Date.parse(stat.mtime),
+        key     = pathname || files[0],
+        headers = {};
+
+    // Copy default headers
+    for (var k in this.options.headers) {  headers[k] = this.options.headers[k] }
+
+    headers['Etag']          = JSON.stringify([stat.ino, stat.size, mtime].join('-'));
+    headers['Date']          = new(Date)().toUTCString();
+    headers['Last-Modified'] = new(Date)(stat.mtime).toUTCString();
+
+    // Conditional GET
+    // If both the "If-Modified-Since" and "If-None-Match" headers
+    // match the conditions, send a 304 Not Modified.
+    if (req.headers['if-none-match'] === headers['Etag'] &&
+        Date.parse(req.headers['if-modified-since']) >= mtime) {
+        finish(304, headers);
+    } else if (req.method === 'HEAD') {
+        finish(200, headers);
+    } else {
+        headers['Content-Length'] = stat.size;
+        headers['Content-Type']   = mime.contentTypes[path.extname(files[0]).slice(1)] ||
+                                   'application/octet-stream';
+
+        for (var k in headers) { _headers[k] = headers[k] }
+
+        res.writeHead(status, _headers);
+
+        // If the file was cached and it's not older
+        // than what's on disk, serve the cached version.
+        if (this.cache && (key in exports.store) &&
+            exports.store[key].stat.mtime >= stat.mtime) {
+            res.end(exports.store[key].buffer);
+            finish(status, _headers);
+        } else {
+            this.stream(pathname, files, new(buffer.Buffer)(stat.size), res, function (e, buffer) {
+                if (e) { return finish(500, {}) }
+                exports.store[key] = {
+                    stat:      stat,
+                    buffer:    buffer,
+                    timestamp: Date.now()
+                };
+                finish(status, _headers);
+            });
+        }
+    }
+};
+
+this.Server.prototype.stream = function (pathname, files, buffer, res, callback) {
+    (function streamFile(files, offset) {
+        var file = files.shift();
+
+        if (file) {
+            file = file[0] === '/' ? file : path.join(pathname || '.', file);
+
+            // Stream the file to the client
+            fs.createReadStream(file, {
+                flags: 'r',
+                mode: 0666
+            }).on('data', function (chunk) {
+                chunk.copy(buffer, offset);
+                offset += chunk.length;
+            }).on('close', function () {
+                streamFile(files, offset);
+            }).on('error', function (err) {
+                callback(err);
+                console.error(err);
+            }).pipe(res, { end: false });
+        } else {
+            res.end();
+            callback(null, buffer, offset);
+        }
+    })(files.slice(0), 0);
+};
diff --git a/node/node_modules/node-static/lib/node-static/mime.js b/node/node_modules/node-static/lib/node-static/mime.js
new file mode 100644 (file)
index 0000000..cdd3355
--- /dev/null
@@ -0,0 +1,139 @@
+this.contentTypes = {
+  "aiff": "audio/x-aiff",
+  "arj": "application/x-arj-compressed",
+  "asf": "video/x-ms-asf",
+  "asx": "video/x-ms-asx",
+  "au": "audio/ulaw",
+  "avi": "video/x-msvideo",
+  "bcpio": "application/x-bcpio",
+  "ccad": "application/clariscad",
+  "cod": "application/vnd.rim.cod",
+  "com": "application/x-msdos-program",
+  "cpio": "application/x-cpio",
+  "cpt": "application/mac-compactpro",
+  "csh": "application/x-csh",
+  "css": "text/css",
+  "deb": "application/x-debian-package",
+  "dl": "video/dl",
+  "doc": "application/msword",
+  "drw": "application/drafting",
+  "dvi": "application/x-dvi",
+  "dwg": "application/acad",
+  "dxf": "application/dxf",
+  "dxr": "application/x-director",
+  "etx": "text/x-setext",
+  "ez": "application/andrew-inset",
+  "fli": "video/x-fli",
+  "flv": "video/x-flv",
+  "gif": "image/gif",
+  "gl": "video/gl",
+  "gtar": "application/x-gtar",
+  "gz": "application/x-gzip",
+  "hdf": "application/x-hdf",
+  "hqx": "application/mac-binhex40",
+  "html": "text/html",
+  "ice": "x-conference/x-cooltalk",
+  "ico": "image/x-icon",
+  "ief": "image/ief",
+  "igs": "model/iges",
+  "ips": "application/x-ipscript",
+  "ipx": "application/x-ipix",
+  "jad": "text/vnd.sun.j2me.app-descriptor",
+  "jar": "application/java-archive",
+  "jpeg": "image/jpeg",
+  "jpg": "image/jpeg",
+  "js": "text/javascript",
+  "json": "application/json",
+  "latex": "application/x-latex",
+  "less": "text/css",
+  "lsp": "application/x-lisp",
+  "lzh": "application/octet-stream",
+  "m": "text/plain",
+  "m3u": "audio/x-mpegurl",
+  "man": "application/x-troff-man",
+  "manifest": "text/cache-manifest",
+  "me": "application/x-troff-me",
+  "midi": "audio/midi",
+  "mif": "application/x-mif",
+  "mime": "www/mime",
+  "movie": "video/x-sgi-movie",
+  "mp4": "video/mp4",
+  "mpg": "video/mpeg",
+  "mpga": "audio/mpeg",
+  "ms": "application/x-troff-ms",
+  "nc": "application/x-netcdf",
+  "oda": "application/oda",
+  "ogm": "application/ogg",
+  "pbm": "image/x-portable-bitmap",
+  "pdf": "application/pdf",
+  "pgm": "image/x-portable-graymap",
+  "pgn": "application/x-chess-pgn",
+  "pgp": "application/pgp",
+  "pm": "application/x-perl",
+  "png": "image/png",
+  "pnm": "image/x-portable-anymap",
+  "ppm": "image/x-portable-pixmap",
+  "ppz": "application/vnd.ms-powerpoint",
+  "pre": "application/x-freelance",
+  "prt": "application/pro_eng",
+  "ps": "application/postscript",
+  "qt": "video/quicktime",
+  "ra": "audio/x-realaudio",
+  "rar": "application/x-rar-compressed",
+  "ras": "image/x-cmu-raster",
+  "rgb": "image/x-rgb",
+  "rm": "audio/x-pn-realaudio",
+  "rpm": "audio/x-pn-realaudio-plugin",
+  "rtf": "text/rtf",
+  "rtx": "text/richtext",
+  "scm": "application/x-lotusscreencam",
+  "set": "application/set",
+  "sgml": "text/sgml",
+  "sh": "application/x-sh",
+  "shar": "application/x-shar",
+  "silo": "model/mesh",
+  "sit": "application/x-stuffit",
+  "skt": "application/x-koan",
+  "smil": "application/smil",
+  "snd": "audio/basic",
+  "sol": "application/solids",
+  "spl": "application/x-futuresplash",
+  "src": "application/x-wais-source",
+  "stl": "application/SLA",
+  "stp": "application/STEP",
+  "sv4cpio": "application/x-sv4cpio",
+  "sv4crc": "application/x-sv4crc",
+  "svg": "image/svg+xml",
+  "swf": "application/x-shockwave-flash",
+  "tar": "application/x-tar",
+  "tcl": "application/x-tcl",
+  "tex": "application/x-tex",
+  "texinfo": "application/x-texinfo",
+  "tgz": "application/x-tar-gz",
+  "tiff": "image/tiff",
+  "tr": "application/x-troff",
+  "tsi": "audio/TSP-audio",
+  "tsp": "application/dsptype",
+  "tsv": "text/tab-separated-values",
+  "txt": "text/plain",
+  "unv": "application/i-deas",
+  "ustar": "application/x-ustar",
+  "vcd": "application/x-cdlink",
+  "vda": "application/vda",
+  "vivo": "video/vnd.vivo",
+  "vrm": "x-world/x-vrml",
+  "wav": "audio/x-wav",
+  "wax": "audio/x-ms-wax",
+  "wma": "audio/x-ms-wma",
+  "wmv": "video/x-ms-wmv",
+  "wmx": "video/x-ms-wmx",
+  "wrl": "model/vrml",
+  "wvx": "video/x-ms-wvx",
+  "xbm": "image/x-xbitmap",
+  "xlw": "application/vnd.ms-excel",
+  "xml": "text/xml",
+  "xpm": "image/x-xpixmap",
+  "xwd": "image/x-xwindowdump",
+  "xyz": "chemical/x-pdb",
+  "zip": "application/zip"
+};
diff --git a/node/node_modules/node-static/lib/node-static/util.js b/node/node_modules/node-static/lib/node-static/util.js
new file mode 100644 (file)
index 0000000..8dd8e04
--- /dev/null
@@ -0,0 +1,30 @@
+var fs = require('fs'),
+    path = require('path');
+
+this.mstat = function (dir, files, callback) {
+    (function mstat(files, stats) {
+        var file = files.shift();
+
+        if (file) {
+            fs.stat(path.join(dir, file), function (e, stat) {
+                if (e) {
+                    callback(e);
+                } else {
+                    mstat(files, stats.concat([stat]));
+                }
+            });
+        } else {
+            callback(null, {
+                size: stats.reduce(function (total, stat) {
+                    return total + stat.size;
+                }, 0),
+                mtime: stats.reduce(function (latest, stat) {
+                    return latest > stat.mtime ? latest : stat.mtime;
+                }, 0),
+                ino: stats.reduce(function (total, stat) {
+                    return total + stat.ino;
+                }, 0)
+            });
+        }
+    })(files.slice(0), []);
+};
diff --git a/node/node_modules/node-static/package.json b/node/node_modules/node-static/package.json
new file mode 100644 (file)
index 0000000..f79326a
--- /dev/null
@@ -0,0 +1,15 @@
+{
+  "name"          : "node-static",
+  "description"   : "simple, compliant file streaming module for node",
+  "url"           : "http://github.com/cloudhead/node-static",
+  "keywords"      : ["http", "static", "file", "server"],
+  "author"        : "Alexis Sellier <self@cloudhead.net>",
+  "contributors"  : [],
+  "licenses"      : ["MIT"],
+  "dependencies"  : [],
+  "lib"           : "lib",
+  "main"          : "./lib/node-static",
+  "version"       : "0.5.6",
+  "directories"   : { "test": "./test" },
+  "engines"       : { "node": ">= 0.4.1" }
+}