mirror of https://github.com/lukechilds/micro.git
Browse Source
* Removed useless package fields * Overkill dependencies removed * Lockfile added * Break down the module usage * Switched to minimist for argument parsing * Cleaned up the code a little * Made LOC number in readme correct * No transpilation happening anymore * micro is leightweight * Replaced minimist with nanomist * Replaced minimist in the code as well * Be more explicit about what lightweight means * How to transpile if not on Node.js 8.0.0 * Improved code example * Use native stream module instead of isstream * Removed --silent option, since the inspection is happening in micro-dev * Properly grouped code * Better name for server handler * Drain current requests before exit This closes #284 * Replaced nanomist with mri * Moved documentation to wiki and linked it * Better documentation section * Splitted into two paragraphs * Moved link to awesome-micro to a different position * Added usage information * Default port added * Made server stoppable on older Node.js versions * Throw error if async/await isn't working * Made function for getting usage info more pure * And even more pure * Brought docs back into readme * No need to set host to null * Removed now-specific code * Default port is 3000 * Only consider prod case in success message * Use npx for travis commands * Added error 10 * Linked errors * Added missing errors * Bumped dependencies to the latest version * Improved code style * Markdown files for missing errors * Corrected exit code for version output * Fixed usage in help * Ability to log the version and missing usage info * Added missing support for version flag * All Node.js versions support the .close() callback * Correct newline for shutdown message * Exit process with correct code * Corrected credits * Stop hiding stack traces when TESTING env var is set * Log errors to stderr * Removed test for not printing errors during test * Better name for error logging function * Only shutdown gracefully on SIGTERM * Errors always have a code * Made clear when to use micro-dev * Bigger logo added * Made logo a little tinier * Adjusted readme heading * Matched description everywhere * Added tim as an authormaster
Leo Lamprecht
7 years ago
committed by
GitHub
22 changed files with 9043 additions and 515 deletions
@ -1,10 +1,7 @@ |
|||
{ |
|||
"language": "node_js", |
|||
"node_js": [ |
|||
"6", |
|||
"node" |
|||
], |
|||
"node_js": "node", |
|||
"after_success": [ |
|||
"./node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls" |
|||
"npx nyc report --reporter=text-lcov | npx coveralls" |
|||
] |
|||
} |
|||
|
@ -0,0 +1,13 @@ |
|||
# Invalid Entry File |
|||
|
|||
#### Why This Error Occurred |
|||
|
|||
When the `micro` command was ran, you passed a path to a file or directory that contains invalid code. This code might either not be syntactically correct or throw an error on execution. |
|||
|
|||
#### Possible Ways to Fix It |
|||
|
|||
The only way to avoid this error is to ensure that the entry file to your microservice (the one passed to the `micro`) command contains code that doesn't contain any syntax errors and doesn't throw an error when executed. |
|||
|
|||
### Useful Links |
|||
|
|||
- [JSLint](http://www.jslint.com) - Validate the code of your entry file |
@ -0,0 +1,9 @@ |
|||
# Invalid `package.json` File |
|||
|
|||
#### Why This Error Occurred |
|||
|
|||
The content of the `package.json` file that's located near the entry file of your microservice is not valid. This means that it's not correct JSON syntax. |
|||
|
|||
#### Possible Ways to Fix It |
|||
|
|||
The only way to avoid this error is to ensure that the file contains a valid JSON object. You can use [JSONLint](https://jsonlint.com) to find any possible errors. |
@ -0,0 +1,20 @@ |
|||
# No Export |
|||
|
|||
#### Why This Error Occurred |
|||
|
|||
When `micro` tried to ran your microservice, it noticed that your code didn't export anything that could be run. |
|||
|
|||
#### Possible Ways to Fix It |
|||
|
|||
You need to ensure that the entry file you passed to the `micro` command contains an export - like this one: |
|||
|
|||
```js |
|||
module.exports = (req, res) => { |
|||
res.end('test') |
|||
} |
|||
``` |
|||
|
|||
### Useful Links |
|||
|
|||
- [List of examples](https://github.com/zeit/micro/tree/master/examples) |
|||
- [Usage information](https://github.com/zeit/micro#usage) |
@ -0,0 +1,10 @@ |
|||
# Old Node Version |
|||
|
|||
#### Why This Error Occurred |
|||
|
|||
In order to execute code that contains the `async` or `await` keywords, you need to have at least version 8.0.0 of [Node.js](https://nodejs.org/en/) installed on the device that runs `micro`. If that's not the case, you will see this error. |
|||
|
|||
#### Possible Ways to Fix It |
|||
|
|||
- Ensure that at least version 8.0.0 of [Node.js](https://nodejs.org/en/) is installed |
|||
- [Transpile](https://github.com/zeit/micro#transpilation) the code of your microservice |
@ -0,0 +1,10 @@ |
|||
# Path Missing |
|||
|
|||
#### Why This Error Occurred |
|||
|
|||
When running the `micro` command, you need to pass a path to a file or directory that contains your microservice. If you don't define one, it will detect the entry file to your code using the `main` property inside the `package.json` file in the directory where the command is run. |
|||
|
|||
#### Possible Ways to Fix It |
|||
|
|||
- Enter the path to your microservice in the `main` property inside `package.json` |
|||
- Specify the path of your entry file when running the command: `micro <path>` |
@ -0,0 +1,13 @@ |
|||
# Path Not Existent |
|||
|
|||
#### Why This Error Occurred |
|||
|
|||
When the `micro` command ran, you passed it a path to a file or directory that does't exist. This is how such a command can look like: |
|||
|
|||
```bash |
|||
micro <not-existing-path> |
|||
``` |
|||
|
|||
#### Possible Ways to Fix It |
|||
|
|||
The only way to fix this is to pass a path to a file or directory that exists and contains a working microservice. |
@ -0,0 +1,4 @@ |
|||
module.exports = (message, errorCode) => { |
|||
console.error(`micro: ${message}`) |
|||
console.error(`micro: https://err.sh/micro/${errorCode}`) |
|||
} |
@ -0,0 +1,46 @@ |
|||
// Native
|
|||
const vm = require('vm') |
|||
|
|||
// Utilities
|
|||
const logError = require('./error') |
|||
|
|||
const checkAsyncAwait = () => { |
|||
try { |
|||
// eslint-disable-next-line no-new
|
|||
new vm.Script('(async () => ({}))()') |
|||
return true |
|||
} catch (err) { |
|||
return false |
|||
} |
|||
} |
|||
|
|||
module.exports = file => { |
|||
let mod |
|||
|
|||
try { |
|||
// eslint-disable-next-line import/no-dynamic-require
|
|||
mod = require(file) |
|||
|
|||
if (mod && typeof mod === 'object') { |
|||
mod = mod.default |
|||
} |
|||
} catch (err) { |
|||
if (!checkAsyncAwait()) { |
|||
logError( |
|||
'In order for `async` & `await` to work, you need to use at least Node.js 8!', |
|||
'old-node-version' |
|||
) |
|||
process.exit(1) |
|||
} |
|||
|
|||
logError(`Error when importing ${file}: ${err.stack}`, 'invalid-entry') |
|||
process.exit(1) |
|||
} |
|||
|
|||
if (typeof mod !== 'function') { |
|||
logError(`The file "${file}" does not export a function.`, 'no-export') |
|||
process.exit(1) |
|||
} |
|||
|
|||
return mod |
|||
} |
@ -0,0 +1,15 @@ |
|||
const green = text => `\x1b[32m${text}\x1b[0m` |
|||
|
|||
module.exports = () => { |
|||
const usage = `\n Usage: ${green('micro')} [path] [options]
|
|||
|
|||
Options: |
|||
|
|||
${green('-p, --port <n>')} Port to listen on (defaults to 3000) |
|||
${green('-H, --host')} The host on which micro will run |
|||
${green('-v, --version')} Output the version number |
|||
${green('-h, --help')} Show this usage information |
|||
` |
|||
|
|||
return usage |
|||
} |
@ -1,41 +1,156 @@ |
|||
// Native
|
|||
const server = require('http').Server |
|||
const { Stream } = require('stream') |
|||
|
|||
// Packages
|
|||
const getPort = require('get-port') |
|||
const getRawBody = require('raw-body') |
|||
const typer = require('media-typer') |
|||
|
|||
const { NODE_ENV } = process.env |
|||
const DEV = NODE_ENV === 'development' |
|||
|
|||
const serve = fn => server((req, res) => exports.run(req, res, fn)) |
|||
|
|||
module.exports = serve |
|||
exports = serve |
|||
exports.default = serve |
|||
|
|||
const createError = (code, message, original) => { |
|||
const err = new Error(message) |
|||
|
|||
err.statusCode = code |
|||
err.originalError = original |
|||
|
|||
return err |
|||
} |
|||
|
|||
const send = (res, code, obj = null) => { |
|||
res.statusCode = code |
|||
|
|||
if (obj === null) { |
|||
res.end() |
|||
return |
|||
} |
|||
|
|||
if (Buffer.isBuffer(obj)) { |
|||
if (!res.getHeader('Content-Type')) { |
|||
res.setHeader('Content-Type', 'application/octet-stream') |
|||
} |
|||
|
|||
res.setHeader('Content-Length', obj.length) |
|||
res.end(obj) |
|||
return |
|||
} |
|||
|
|||
// Ours
|
|||
const serve = require('./server') |
|||
const listening = require('./listening') |
|||
const getModule = require('./module') |
|||
if (obj instanceof Stream) { |
|||
if (!res.getHeader('Content-Type')) { |
|||
res.setHeader('Content-Type', 'application/octet-stream') |
|||
} |
|||
|
|||
module.exports = async (file, flags, module = getModule(file)) => { |
|||
const server = serve(module) |
|||
obj.pipe(res) |
|||
return |
|||
} |
|||
|
|||
let port = flags.port |
|||
let host = flags.host |
|||
let str = obj |
|||
|
|||
const open = await getPort(port) |
|||
let inUse = open !== port |
|||
if (typeof obj === 'object' || typeof obj === 'number') { |
|||
// We stringify before setting the header
|
|||
// in case `JSON.stringify` throws and a
|
|||
// 500 has to be sent instead
|
|||
|
|||
if (inUse) { |
|||
port = open |
|||
// the `JSON.stringify` call is split into
|
|||
// two cases as `JSON.stringify` is optimized
|
|||
// in V8 if called with only one argument
|
|||
if (DEV) { |
|||
str = JSON.stringify(obj, null, 2) |
|||
} else { |
|||
str = JSON.stringify(obj) |
|||
} |
|||
|
|||
if (!res.getHeader('Content-Type')) { |
|||
res.setHeader('Content-Type', 'application/json') |
|||
} |
|||
} |
|||
|
|||
inUse = { |
|||
old: flags.port, |
|||
open |
|||
res.setHeader('Content-Length', Buffer.byteLength(str)) |
|||
res.end(str) |
|||
} |
|||
|
|||
const sendError = (req, res, { statusCode, status, message, stack }) => { |
|||
statusCode = statusCode || status |
|||
|
|||
if (statusCode) { |
|||
send(res, statusCode, DEV ? stack : message) |
|||
} else { |
|||
send(res, 500, DEV ? stack : 'Internal Server Error') |
|||
} |
|||
|
|||
if (host === '0.0.0.0') { |
|||
host = null |
|||
console.error(stack) |
|||
} |
|||
|
|||
server.on('error', err => { |
|||
console.error('micro:', err.stack) |
|||
exports.send = send |
|||
exports.sendError = sendError |
|||
exports.createError = createError |
|||
|
|||
exports.run = (req, res, fn) => |
|||
new Promise(resolve => resolve(fn(req, res))) |
|||
.then(val => { |
|||
if (val === null) { |
|||
send(res, 204, null) |
|||
return |
|||
} |
|||
|
|||
// eslint-disable-next-line unicorn/no-process-exit
|
|||
process.exit(1) |
|||
// Send value if it is not undefined, otherwise assume res.end
|
|||
// will be called later
|
|||
if (undefined !== val) { |
|||
send(res, res.statusCode || 200, val) |
|||
} |
|||
}) |
|||
.catch(err => sendError(req, res, err)) |
|||
|
|||
// Maps requests to buffered raw bodies so that
|
|||
// multiple calls to `json` work as expected
|
|||
const rawBodyMap = new WeakMap() |
|||
|
|||
const parseJSON = str => { |
|||
try { |
|||
return JSON.parse(str) |
|||
} catch (err) { |
|||
throw createError(400, 'Invalid JSON', err) |
|||
} |
|||
} |
|||
|
|||
server.listen(port, host, () => { |
|||
return listening(server, inUse, flags.silent) |
|||
exports.buffer = (req, { limit = '1mb', encoding } = {}) => |
|||
Promise.resolve().then(() => { |
|||
const type = req.headers['content-type'] || 'text/plain' |
|||
const length = req.headers['content-length'] |
|||
|
|||
if (encoding === undefined) { |
|||
encoding = typer.parse(type).parameters.charset |
|||
} |
|||
|
|||
const body = rawBodyMap.get(req) |
|||
|
|||
if (body) { |
|||
return body |
|||
} |
|||
|
|||
return getRawBody(req, { limit, length, encoding }) |
|||
.then(buf => { |
|||
rawBodyMap.set(req, buf) |
|||
return buf |
|||
}) |
|||
.catch(err => { |
|||
if (err.type === 'entity.too.large') { |
|||
throw createError(413, `Body exceeded ${limit} limit`, err) |
|||
} else { |
|||
throw createError(400, 'Invalid body', err) |
|||
} |
|||
}) |
|||
}) |
|||
|
|||
exports.text = (req, { limit, encoding } = {}) => |
|||
exports.buffer(req, { limit, encoding }).then(body => body.toString(encoding)) |
|||
|
|||
exports.json = (req, opts) => |
|||
exports.text(req, opts).then(body => parseJSON(body)) |
|||
|
@ -1,62 +0,0 @@ |
|||
// Packages
|
|||
const { write: copy } = require('clipboardy') |
|||
const ip = require('ip') |
|||
const chalk = require('chalk') |
|||
const boxen = require('boxen') |
|||
|
|||
const copyToClipboard = async text => { |
|||
try { |
|||
await copy(text) |
|||
return true |
|||
} catch (err) { |
|||
return false |
|||
} |
|||
} |
|||
|
|||
module.exports = async (server, inUse, silent) => { |
|||
const details = server.address() |
|||
const ipAddress = ip.address() |
|||
const url = `http://${ipAddress}:${details.port}` |
|||
|
|||
const isTTY = process.stdout.isTTY |
|||
|
|||
process.on('SIGINT', () => { |
|||
server.close() |
|||
process.exit(0) |
|||
}) |
|||
|
|||
if (!(silent || process.env.NOW)) { |
|||
let message = chalk.green('Micro is running!') |
|||
|
|||
if (inUse) { |
|||
message += |
|||
' ' + |
|||
chalk.red( |
|||
`(on port ${inUse.open}, because ${inUse.old} is already in use)` |
|||
) |
|||
} |
|||
|
|||
message += '\n\n' |
|||
|
|||
const localURL = `http://localhost:${details.port}` |
|||
|
|||
message += `• ${chalk.bold('Local: ')} ${localURL}\n` |
|||
message += `• ${chalk.bold('On Your Network: ')} ${url}\n\n` |
|||
|
|||
if (isTTY) { |
|||
const copied = await copyToClipboard(localURL) |
|||
|
|||
if (copied) { |
|||
message += `${chalk.grey('Copied local address to clipboard!')}` |
|||
} |
|||
} |
|||
|
|||
console.log( |
|||
boxen(message, { |
|||
padding: 1, |
|||
borderColor: 'green', |
|||
margin: 1 |
|||
}) |
|||
) |
|||
} |
|||
} |
@ -1,26 +0,0 @@ |
|||
module.exports = file => { |
|||
let mod |
|||
|
|||
try { |
|||
// eslint-disable-next-line import/no-dynamic-require
|
|||
mod = require(file) |
|||
|
|||
if (mod && typeof mod === 'object') { |
|||
mod = mod.default |
|||
} |
|||
} catch (err) { |
|||
console.error(`micro: Error when importing ${file}: ${err.stack}`) |
|||
|
|||
// eslint-disable-next-line unicorn/no-process-exit
|
|||
process.exit(1) |
|||
} |
|||
|
|||
if (typeof mod !== 'function') { |
|||
console.error(`micro: "${file}" does not export a function.`) |
|||
|
|||
// eslint-disable-next-line unicorn/no-process-exit
|
|||
process.exit(1) |
|||
} |
|||
|
|||
return mod |
|||
} |
@ -1,156 +0,0 @@ |
|||
// Native
|
|||
const server = require('http').Server |
|||
|
|||
// Packages
|
|||
const getRawBody = require('raw-body') |
|||
const typer = require('media-typer') |
|||
const isStream = require('isstream') |
|||
const Promise = require('bluebird') |
|||
|
|||
const DEV = process.env.NODE_ENV === 'development' |
|||
const TESTING = process.env.NODE_ENV === 'test' |
|||
|
|||
const serve = fn => server((req, res) => exports.run(req, res, fn)) |
|||
|
|||
module.exports = serve |
|||
exports = serve |
|||
exports.default = serve |
|||
|
|||
exports.send = send |
|||
exports.sendError = sendError |
|||
exports.createError = createError |
|||
|
|||
exports.run = (req, res, fn) => |
|||
new Promise(resolve => resolve(fn(req, res))) |
|||
.then(val => { |
|||
if (val === null) { |
|||
send(res, 204, null) |
|||
return |
|||
} |
|||
|
|||
// Send value if it is not undefined, otherwise assume res.end
|
|||
// will be called later
|
|||
if (undefined !== val) { |
|||
send(res, res.statusCode || 200, val) |
|||
} |
|||
}) |
|||
.catch(err => sendError(req, res, err)) |
|||
|
|||
// Maps requests to buffered raw bodies so that
|
|||
// multiple calls to `json` work as expected
|
|||
const rawBodyMap = new WeakMap() |
|||
|
|||
const parseJSON = str => { |
|||
try { |
|||
return JSON.parse(str) |
|||
} catch (err) { |
|||
throw createError(400, 'Invalid JSON', err) |
|||
} |
|||
} |
|||
|
|||
exports.buffer = (req, { limit = '1mb', encoding } = {}) => |
|||
Promise.resolve().then(() => { |
|||
const type = req.headers['content-type'] || 'text/plain' |
|||
const length = req.headers['content-length'] |
|||
encoding = encoding === undefined |
|||
? typer.parse(type).parameters.charset |
|||
: encoding |
|||
|
|||
const body = rawBodyMap.get(req) |
|||
|
|||
if (body) { |
|||
return body |
|||
} |
|||
|
|||
return getRawBody(req, { limit, length, encoding }) |
|||
.then(buf => { |
|||
rawBodyMap.set(req, buf) |
|||
return buf |
|||
}) |
|||
.catch(err => { |
|||
if (err.type === 'entity.too.large') { |
|||
throw createError(413, `Body exceeded ${limit} limit`, err) |
|||
} else { |
|||
throw createError(400, 'Invalid body', err) |
|||
} |
|||
}) |
|||
}) |
|||
|
|||
exports.text = (req, { limit, encoding } = {}) => |
|||
exports.buffer(req, { limit, encoding }).then(body => body.toString(encoding)) |
|||
|
|||
exports.json = (req, opts) => |
|||
exports.text(req, opts).then(body => parseJSON(body)) |
|||
|
|||
function send(res, code, obj = null) { |
|||
res.statusCode = code |
|||
|
|||
if (obj === null) { |
|||
res.end() |
|||
return |
|||
} |
|||
|
|||
if (Buffer.isBuffer(obj)) { |
|||
if (!res.getHeader('Content-Type')) { |
|||
res.setHeader('Content-Type', 'application/octet-stream') |
|||
} |
|||
|
|||
res.setHeader('Content-Length', obj.length) |
|||
res.end(obj) |
|||
return |
|||
} |
|||
|
|||
if (isStream(obj)) { |
|||
if (!res.getHeader('Content-Type')) { |
|||
res.setHeader('Content-Type', 'application/octet-stream') |
|||
} |
|||
|
|||
obj.pipe(res) |
|||
return |
|||
} |
|||
|
|||
let str = obj |
|||
|
|||
if (typeof obj === 'object' || typeof obj === 'number') { |
|||
// We stringify before setting the header
|
|||
// in case `JSON.stringify` throws and a
|
|||
// 500 has to be sent instead
|
|||
|
|||
// the `JSON.stringify` call is split into
|
|||
// two cases as `JSON.stringify` is optimized
|
|||
// in V8 if called with only one argument
|
|||
if (DEV) { |
|||
str = JSON.stringify(obj, null, 2) |
|||
} else { |
|||
str = JSON.stringify(obj) |
|||
} |
|||
|
|||
if (!res.getHeader('Content-Type')) { |
|||
res.setHeader('Content-Type', 'application/json') |
|||
} |
|||
} |
|||
|
|||
res.setHeader('Content-Length', Buffer.byteLength(str)) |
|||
res.end(str) |
|||
} |
|||
|
|||
function sendError(req, res, { statusCode, status, message, stack }) { |
|||
statusCode = statusCode || status |
|||
|
|||
if (statusCode) { |
|||
send(res, statusCode, DEV ? stack : message) |
|||
} else { |
|||
send(res, 500, DEV ? stack : 'Internal Server Error') |
|||
} |
|||
|
|||
if (!TESTING) { |
|||
console.error(stack) |
|||
} |
|||
} |
|||
|
|||
function createError(code, msg, orig) { |
|||
const err = new Error(msg) |
|||
err.statusCode = code |
|||
err.originalError = orig |
|||
return err |
|||
} |
File diff suppressed because it is too large
@ -1,34 +0,0 @@ |
|||
// Packages
|
|||
const test = require('ava') |
|||
const request = require('request-promise') |
|||
const listen = require('test-listen') |
|||
|
|||
process.env.NODE_ENV = 'test' |
|||
const micro = require('../lib/server') |
|||
|
|||
const getUrl = fn => { |
|||
const srv = micro(fn) |
|||
|
|||
return listen(srv) |
|||
} |
|||
|
|||
test.serial('errors are not printed in console in testing', async t => { |
|||
let logged = false |
|||
const _error = console.error |
|||
console.error = () => { |
|||
logged = true |
|||
} |
|||
|
|||
const fn = () => { |
|||
throw new Error('Bang') |
|||
} |
|||
|
|||
const url = await getUrl(fn) |
|||
try { |
|||
await request(url) |
|||
} catch (err) { |
|||
t.false(logged) |
|||
t.deepEqual(err.statusCode, 500) |
|||
console.error = _error |
|||
} |
|||
}) |
Loading…
Reference in new issue