aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRagnis Armus <ragnis@armus.ee>2019-12-25 15:20:03 +0200
committerRagnis Armus <ragnis@armus.ee>2019-12-25 15:20:03 +0200
commitbfb346fb24fcbf00b38c71d4763bdc64d7b4a4c2 (patch)
tree3a2ed00608acbde3db1fc3b47b6a16be1483d96b
First commit
-rw-r--r--LICENSE21
-rw-r--r--README.md15
-rwxr-xr-xdpm375
-rw-r--r--example/.dockerignore1
-rw-r--r--example/.gitignore1
-rw-r--r--example/Dockerfile8
-rw-r--r--example/index.js10
-rw-r--r--example/package-lock.json372
-rw-r--r--example/package.json8
9 files changed, 811 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f9e47fc
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Ragnis Armus
+
+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/README.md b/README.md
new file mode 100644
index 0000000..f62d921
--- /dev/null
+++ b/README.md
@@ -0,0 +1,15 @@
+# dpm
+
+dpm is a helper for running Node.js services in Docker containers.
+
+Example:
+
+ cd ./example
+
+ # Build a container, and execute the command "start" in a container:
+ dpm
+
+ # Execute a custom command:
+ dpm -- echo hello world
+
+Check out `dpm -h` and `dpm start -h` for additional options.
diff --git a/dpm b/dpm
new file mode 100755
index 0000000..3bebdc7
--- /dev/null
+++ b/dpm
@@ -0,0 +1,375 @@
+#!/usr/bin/python3
+
+import argparse
+import hashlib
+import json
+import os
+import os.path
+import re
+import subprocess
+import sys
+
+
+class ValidationError(Exception):
+ pass
+
+
+def fatal(*args, **kwargs):
+ print(*args, file=sys.stderr, **kwargs)
+ sys.exit(1)
+
+
+def exec(args):
+ os.execvp(args[0], args)
+
+
+def parse_args(parser, args):
+ subparser_found = False
+
+ for arg in args:
+ if arg in ['-h', '--help']:
+ break
+ else:
+ if len(args) > 0:
+ for x in parser._subparsers._actions:
+ if not isinstance(x, argparse._SubParsersAction):
+ continue
+
+ for sp_name in x._name_parser_map.keys():
+ if sp_name == args[0]:
+ subparser_found = True
+
+ if subparser_found:
+ break
+
+ if not subparser_found:
+ args.insert(0, 'start')
+
+ return parser.parse_args(args)
+
+
+def hash_file_sha1(filename):
+ hash = hashlib.sha1()
+
+ try:
+ with open(filename, 'rb') as f:
+ for chunk in iter(lambda: f.read(4096), b''):
+ hash.update(chunk)
+ except FileNotFoundError:
+ pass
+
+ return hash.hexdigest()
+
+
+def hash_files(files):
+ hash = hashlib.sha1()
+
+ for f in files:
+ hash.update(hash_file_sha1(f).encode('ascii'))
+
+ return hash.hexdigest()
+
+
+def read_lines(filename):
+ with open(filename, 'rb') as f:
+ for line in f.readlines():
+ yield line.decode('utf8')
+
+
+def read_dict(filename, mapper):
+ rv = {}
+ for line in read_lines(filename):
+ parsed = mapper(line)
+ if parsed is not None:
+ (k, v) = parsed
+ rv[k] = v
+ return rv
+
+
+def npmrc_parse_line(line):
+ m = re.match(r'(.+?)=(.*)$', line)
+ if m is None:
+ return None
+
+ key = m.group(1)
+ val = m.group(2)
+
+ if len(val) >= 2 and val[0] == '"':
+ val = val[1:-1]
+ val = re.sub(r'\\\\', r'\\', val)
+ val = re.sub(r'\\"', r'"', val)
+
+ return (key, val)
+
+
+def dotenv_parse_line(line):
+ if not line or line[0] == '#':
+ return None
+
+ m = re.match(r'(.+?)=(.*)$', line)
+ if m is None:
+ return None
+
+ return (m.group(1), m.group(2))
+
+
+def get_project_name(dir):
+ try:
+ with open(os.path.join(dir, 'package.json'), 'r') as f:
+ pkg = json.loads(f.read())
+
+ if 'name' in pkg:
+ return pkg['name']
+ except:
+ pass
+
+ return os.path.basename(dir)
+
+
+def get_npm_token():
+ if 'NPM_CONFIG_USERCONFIG' in os.environ:
+ filename = os.environ['NPM_CONFIG_USERCONFIG']
+ else:
+ filename = path.join(os.environ['HOME'], '.npmrc')
+
+ try:
+ rc = read_dict(filename, npmrc_parse_line)
+ except FileNotFoundError:
+ return None
+
+ if 'registry' not in rc:
+ return None
+
+ reg = re.sub(r'^https?://(.+)$', r'\1', rc['registry'])
+ key = '//{}:_authToken'.format(reg)
+
+ if key in rc:
+ return rc[key]
+
+
+def run_for_output(args):
+ cp = subprocess.run(args, capture_output=True)
+ cp.check_returncode()
+ return cp
+
+
+def list_docker_things(kind, filters):
+ cmd = ['docker'] + kind + ['--no-trunc', '-q']
+
+ for filter in filters:
+ cmd.extend(('--filter', filter))
+
+ out = run_for_output(cmd).stdout
+ lines = out.decode('utf8').split('\n')
+ things = []
+
+ for line in lines:
+ if len(line) > 0:
+ things.append(line)
+
+ return things
+
+
+def inspect_image(id):
+ out = run_for_output(['docker', 'image', 'inspect', id]).stdout
+ images = json.loads(out)
+
+ if not images:
+ return None
+
+ return {
+ 'id': images[0]['Id'],
+ 'workdir': images[0]['Config']['WorkingDir'],
+ }
+
+
+def find_image(name, hash):
+ images = list_docker_things(['images'], [
+ 'label=ee.radr.dpm.project-name=' + name,
+ 'label=ee.radr.dpm.project-hash=' + hash,
+ ])
+
+ if len(images) > 0:
+ return inspect_image(images[0])
+
+
+def dockerfile_has_arg(filename, arg):
+ for line in read_lines(filename):
+ m = re.match(r'ARG\s+(.+)$', line)
+ if m and m.group(1) == arg:
+ return True
+ return False
+
+
+def build_image(name, hash):
+ dockerfile = 'Dockerfile'
+ cmd = [
+ 'docker',
+ 'build',
+ '-f', dockerfile,
+ '--label', 'ee.radr.dpm.project-name=' + name,
+ '--label', 'ee.radr.dpm.project-hash=' + hash,
+ '.'
+ ]
+
+ if dockerfile_has_arg(dockerfile, 'NPM_TOKEN'):
+ token = get_npm_token()
+
+ print(token)
+
+ if not token:
+ raise Exception('Dockerfile requires NPM token but none could be found')
+
+ cmd.append('--build-arg')
+ cmd.append('NPM_TOKEN=' + token)
+
+ cp = subprocess.run(cmd,
+ stdout=sys.stdout,
+ stderr=sys.stderr)
+
+ if cp.returncode != 0:
+ raise Exception('could not build image')
+
+ return find_image(name, hash)
+
+
+def ensure_image(name, hash, build=False):
+ image = None
+ if not build:
+ image = find_image(name, hash)
+ if image is None:
+ image = build_image(name, hash)
+ return image
+
+
+def run_container(name, image, bg, cmd):
+ args = ['--rm', '-ti', '--name', name, '--hostname', name]
+
+ if bg:
+ args.append('-d')
+
+ try:
+ dotenv = read_dict('.env', dotenv_parse_line)
+ except FileNotFoundError:
+ dotenv = None
+
+ if dotenv:
+ for k, v in dotenv.items():
+ args.extend(('-e', k + '=' + v))
+
+ if 'PORT' in dotenv:
+ port = dotenv['PORT']
+ args.extend(('-p', port + ':' + port))
+
+ if image['workdir']:
+ args.extend(('--mount', 'type=bind,src={},dst={},readonly'.format(
+ os.getcwd(),
+ image['workdir'])))
+
+ args.extend(('--volume', image['workdir'] + '/node_modules'))
+
+ exec(['docker', 'run'] + args + [image['id']] + cmd)
+
+
+def get_container_id(project_name):
+ ids = list_docker_things(['ps'], [
+ 'label=ee.radr.dpm.project-name=' + project_name
+ ])
+
+ if len(ids) > 0:
+ return ids[0]
+
+
+def action_start(args):
+ build = args.build
+
+ project_name = get_project_name(os.getcwd())
+ project_hash = hash_files([
+ 'package.json',
+ 'package-lock.json',
+ ])
+
+ id = get_container_id(project_name)
+ if not id and args.replace:
+ fatal('Cannot replace; no container is running')
+ elif id and not args.replace:
+ fatal(
+ 'Already running. Pass either --replace to restart, or use the '
+ '"attach" command to bring to foreground')
+
+ image = ensure_image(project_name, project_hash, build)
+ if image is None:
+ fatal('No image exists and none was built')
+
+ if id and args.replace:
+ subprocess.run(['docker', 'stop', id], check=True)
+
+ run_container(project_name, image, args.detach, args.cmd)
+
+
+def action_stop():
+ project_name = get_project_name(os.getcwd())
+ id = get_container_id(project_name)
+
+ if not id:
+ raise ValidationError('Cannot stop an already stopped or inexistent container')
+
+ exec(['docker', 'stop', id])
+
+
+def action_kill(args):
+ project_name = get_project_name(os.getcwd())
+ id = get_container_id(project_name)
+
+ if not id:
+ raise ValidationError('Cannot send a signal to an already stopped or inexistent container')
+
+ exec(['docker', 'kill', '-s', args.signal, id])
+
+
+def action_attach():
+ project_name = get_project_name(os.getcwd())
+ id = get_container_id(project_name)
+
+ if not id:
+ raise ValidationError('Cannot attach to a stopped or inexistent container')
+
+ exec(['docker', 'attach', id])
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ subparsers = parser.add_subparsers(dest='action')
+
+ subparsers.add_parser('help', add_help=False)
+ subparsers.add_parser('stop')
+ subparsers.add_parser('attach')
+
+ parser_start = subparsers.add_parser('start')
+ parser_start.add_argument('-b', '--build', action='store_true')
+ parser_start.add_argument('-d', '--detach', action='store_true')
+ parser_start.add_argument('--replace', action='store_true')
+ parser_start.add_argument('cmd', nargs=argparse.REMAINDER)
+
+ parser_kill = subparsers.add_parser('kill')
+ parser_kill.add_argument('signal', nargs='?', default='KILL')
+
+ args = parse_args(parser, sys.argv[1:])
+
+ try:
+ if args.action == 'start':
+ if len(args.cmd) > 0 and args.cmd[0] == '--':
+ args.cmd = args.cmd[1:]
+ if len(args.cmd) == 0:
+ args.cmd = ['start']
+ action_start(args)
+ elif args.action == 'stop':
+ action_stop()
+ elif args.action == 'kill':
+ action_kill(args)
+ elif args.action == 'attach':
+ action_attach()
+ elif args.action == 'help':
+ parser.print_help()
+ except ValidationError as e:
+ fatal(e)
diff --git a/example/.dockerignore b/example/.dockerignore
new file mode 100644
index 0000000..c2658d7
--- /dev/null
+++ b/example/.dockerignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/example/.gitignore b/example/.gitignore
new file mode 100644
index 0000000..c2658d7
--- /dev/null
+++ b/example/.gitignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/example/Dockerfile b/example/Dockerfile
new file mode 100644
index 0000000..58a905c
--- /dev/null
+++ b/example/Dockerfile
@@ -0,0 +1,8 @@
+FROM node:10
+WORKDIR /app
+
+COPY . /app
+RUN npm ci
+
+USER node
+ENTRYPOINT ["npm", "run"]
diff --git a/example/index.js b/example/index.js
new file mode 100644
index 0000000..b39377d
--- /dev/null
+++ b/example/index.js
@@ -0,0 +1,10 @@
+const express = require('express');
+const app = express();
+
+app.get('/', (req, res) => {
+ res.send('Hello World!');
+});
+
+app.listen(3000, () => {
+ console.log('Example app listening on port 3000!');
+});
diff --git a/example/package-lock.json b/example/package-lock.json
new file mode 100644
index 0000000..7ab3a50
--- /dev/null
+++ b/example/package-lock.json
@@ -0,0 +1,372 @@
+{
+ "requires": true,
+ "lockfileVersion": 1,
+ "dependencies": {
+ "accepts": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
+ "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
+ "requires": {
+ "mime-types": "~2.1.24",
+ "negotiator": "0.6.2"
+ }
+ },
+ "array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+ },
+ "body-parser": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
+ "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
+ "requires": {
+ "bytes": "3.1.0",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
+ "on-finished": "~2.3.0",
+ "qs": "6.7.0",
+ "raw-body": "2.4.0",
+ "type-is": "~1.6.17"
+ }
+ },
+ "bytes": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
+ },
+ "content-disposition": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
+ "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
+ "requires": {
+ "safe-buffer": "5.1.2"
+ }
+ },
+ "content-type": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
+ },
+ "cookie": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
+ "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
+ },
+ "cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
+ },
+ "destroy": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+ },
+ "ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+ },
+ "encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
+ },
+ "escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+ },
+ "etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+ },
+ "express": {
+ "version": "4.17.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
+ "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
+ "requires": {
+ "accepts": "~1.3.7",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.19.0",
+ "content-disposition": "0.5.3",
+ "content-type": "~1.0.4",
+ "cookie": "0.4.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.1.2",
+ "fresh": "0.5.2",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.5",
+ "qs": "6.7.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.1.2",
+ "send": "0.17.1",
+ "serve-static": "1.14.1",
+ "setprototypeof": "1.1.1",
+ "statuses": "~1.5.0",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ }
+ },
+ "finalhandler": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+ "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+ "requires": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "statuses": "~1.5.0",
+ "unpipe": "~1.0.0"
+ }
+ },
+ "forwarded": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+ "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
+ },
+ "fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
+ },
+ "http-errors": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
+ "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
+ "requires": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.1",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.0"
+ }
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+ },
+ "ipaddr.js": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
+ "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA=="
+ },
+ "media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+ },
+ "merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+ },
+ "methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
+ },
+ "mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
+ },
+ "mime-db": {
+ "version": "1.42.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz",
+ "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ=="
+ },
+ "mime-types": {
+ "version": "2.1.25",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.25.tgz",
+ "integrity": "sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg==",
+ "requires": {
+ "mime-db": "1.42.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "negotiator": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
+ },
+ "on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
+ "parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
+ },
+ "path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+ },
+ "proxy-addr": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz",
+ "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==",
+ "requires": {
+ "forwarded": "~0.1.2",
+ "ipaddr.js": "1.9.0"
+ }
+ },
+ "qs": {
+ "version": "6.7.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
+ },
+ "range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
+ },
+ "raw-body": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
+ "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
+ "requires": {
+ "bytes": "3.1.0",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "send": {
+ "version": "0.17.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
+ "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
+ "requires": {
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "destroy": "~1.0.4",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "~1.7.2",
+ "mime": "1.6.0",
+ "ms": "2.1.1",
+ "on-finished": "~2.3.0",
+ "range-parser": "~1.2.1",
+ "statuses": "~1.5.0"
+ },
+ "dependencies": {
+ "ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+ }
+ }
+ },
+ "serve-static": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
+ "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
+ "requires": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.17.1"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
+ },
+ "statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
+ },
+ "toidentifier": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
+ },
+ "type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "requires": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ }
+ },
+ "unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+ },
+ "utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
+ },
+ "vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
+ }
+ }
+}
diff --git a/example/package.json b/example/package.json
new file mode 100644
index 0000000..af383f7
--- /dev/null
+++ b/example/package.json
@@ -0,0 +1,8 @@
+{
+ "dependencies": {
+ "express": "^4.17.1"
+ },
+ "scripts": {
+ "start": "node index.js"
+ }
+}