diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 2006df9d..bc5048d3 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -11,9 +11,11 @@ jobs:
node-version:
- 16
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v1
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- - run: npm install
- - run: npm test
+ - name: Install dependencies
+ run: yarn
+ - name: Run tests
+ run: yarn run test
diff --git a/.gitignore b/.gitignore
index a3d1771d..17f9cd1e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,6 @@ yarn-error.log
# coverage
coverage
.nyc_output
+
+# build
+dist/
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 00000000..83584d4a
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,2 @@
+save-exact = true
+strict-peer-dependencies=false
\ No newline at end of file
diff --git a/package.json b/package.json
index c31bab1a..9be16e31 100644
--- a/package.json
+++ b/package.json
@@ -1,24 +1,17 @@
{
"private": true,
"workspaces": [
- "packages/*"
+ "packages/*",
+ "test"
],
"scripts": {
- "test": "NODE_ENV=test nyc --check-coverage --statements 100 --branches 100 --functions 100 --lines 100 ava",
"prepublish": "lerna run prepublish",
"publish-canary": "lerna version prerelease --preid canary --force-publish && release --pre",
- "publish-stable": "lerna version --force-publish"
+ "publish-stable": "lerna version --force-publish",
+ "test": "cd test && yarn run test"
},
"license": "MIT",
"devDependencies": {
- "ava": "0.23.0",
- "lerna": "^3.4.0",
- "node-fetch": "2.6.0",
- "nyc": "11.3.0",
- "resumer": "0.0.0",
- "rewire": "3.0.2",
- "sinon": "4.4.3",
- "test-listen": "1.0.2",
- "then-sleep": "1.0.1"
+ "lerna": "^3.4.0"
}
}
diff --git a/packages/micro/.eslintrc.js b/packages/micro/.eslintrc.js
new file mode 100644
index 00000000..63ecdaf2
--- /dev/null
+++ b/packages/micro/.eslintrc.js
@@ -0,0 +1,12 @@
+module.exports = {
+ root: true,
+ extends: [
+ require.resolve('@vercel/style-guide/eslint/node'),
+ require.resolve('@vercel/style-guide/eslint/typescript'),
+ ],
+ parserOptions: {
+ tsconfigRootDir: __dirname,
+ project: ['./tsconfig.json'],
+ },
+ ignorePatterns: ['dist/**', 'types/**'],
+};
diff --git a/packages/micro/README.md b/packages/micro/README.md
index b9a7c15f..4e922b2e 100644
--- a/packages/micro/README.md
+++ b/packages/micro/README.md
@@ -214,21 +214,23 @@ module.exports = async (req, res) => {
You can use Micro programmatically by requiring Micro directly:
```js
-const micro = require('micro')
-const sleep = require('then-sleep')
+const http = require('http');
+const { serve } = require('micro');
+const sleep = require('then-sleep');
-const server = micro(async (req, res) => {
- await sleep(500)
- return 'Hello world'
-})
+const server = new http.Server(
+ serve(async (req, res) => {
+ await sleep(500);
+ return 'Hello world';
+ }),
+);
-server.listen(3000)
+server.listen(3000);
```
-##### micro(fn)
+##### serve(fn)
-- This function is exposed as the `default` export.
-- Use `require('micro')`.
+- Use `require('micro').serve`.
- Returns a function with the `(req, res) => void` signature. That uses the provided `function` as the request handler.
- The supplied function is run with `await`. So it can be `async`
@@ -320,22 +322,22 @@ module.exports = handleErrors(async (req, res) => {
## Testing
Micro makes tests compact and a pleasure to read and write.
-We recommend [ava](https://github.com/sindresorhus/ava), a highly parallel Micro test framework with built-in support for async tests:
+We recommend [Node TAP](https://node-tap.org/) or [AVA](https://github.com/avajs/ava), a highly parallel test framework with built-in support for async tests:
```js
const http = require('http');
-const micro = require('micro');
+const { send, serve } = require('micro');
const test = require('ava');
const listen = require('test-listen');
const fetch = require('node-fetch');
test('my endpoint', async (t) => {
const service = new http.Server(
- micro(async (req, res) => {
- micro.send(res, 200, {
+ serve(async (req, res) => {
+ send(res, 200, {
test: 'woot',
});
- })
+ }),
);
const url = await listen(service);
@@ -356,7 +358,7 @@ function that returns a URL with an ephemeral port every time it's called.
2. Link the package to the global module directory: `npm link`
3. Within the module you want to test your local development instance of Micro, just link it to the dependencies: `npm link micro`. Instead of the default one from npm, node will now use your clone of Micro!
-You can run the [AVA](https://github.com/sindresorhus/ava) tests using: `npm test`
+You can run the tests using: `npm test`.
## Credits
diff --git a/packages/micro/bin/micro.js b/packages/micro/bin/micro.js
deleted file mode 100755
index 01b5cb44..00000000
--- a/packages/micro/bin/micro.js
+++ /dev/null
@@ -1,233 +0,0 @@
-#!/usr/bin/env node
-
-// Native
-const http = require('http');
-const path = require('path');
-const {existsSync} = require('fs');
-
-// Packages
-const arg = require('arg');
-
-// Utilities
-const serve = require('../lib');
-const handle = require('../lib/handler');
-const {version} = require('../package');
-const logError = require('../lib/error');
-const parseEndpoint = require('../lib/parse-endpoint.js');
-
-// Check if the user defined any options
-const args = arg({
- '--listen': [parseEndpoint],
- '-l': '--listen',
-
- '--help': Boolean,
-
- '--version': Boolean,
- '-v': '--version',
-
- // Deprecated options
- '--port': Number,
- '-p': '--port',
- '--host': String,
- '-h': '--host',
- '--unix-socket': String,
- '-s': '--unix-socket'
-});
-
-// When `-h` or `--help` are used, print out
-// the usage information
-if (args['--help']) {
- console.error(`
- micro - Asynchronous HTTP microservices
-
- USAGE
-
- $ micro --help
- $ micro --version
- $ micro [-l listen_uri [-l ...]] [entry_point.js]
-
- By default micro will listen on 0.0.0.0:3000 and will look first
- for the "main" property in package.json and subsequently for index.js
- as the default entry_point.
-
- Specifying a single --listen argument will overwrite the default, not supplement it.
-
- OPTIONS
-
- --help shows this help message
-
- -v, --version displays the current version of micro
-
- -l, --listen listen_uri specify a URI endpoint on which to listen (see below) -
- more than one may be specified to listen in multiple places
-
- ENDPOINTS
-
- Listen endpoints (specified by the --listen or -l options above) instruct micro
- to listen on one or more interfaces/ports, UNIX domain sockets, or Windows named pipes.
-
- For TCP (traditional host/port) endpoints:
-
- $ micro -l tcp://hostname:1234
-
- For UNIX domain socket endpoints:
-
- $ micro -l unix:/path/to/socket.sock
-
- For Windows named pipe endpoints:
-
- $ micro -l pipe:\\\\.\\pipe\\PipeName
-`);
- process.exit(2);
-}
-
-// Print out the package's version when
-// `--version` or `-v` are used
-if (args['--version']) {
- console.log(version);
- process.exit();
-}
-
-if ((args['--port'] || args['--host']) && args['--unix-socket']) {
- logError(
- `Both host/port and socket provided. You can only use one.`,
- 'invalid-port-socket'
- );
- process.exit(1);
-}
-
-let deprecatedEndpoint = null;
-
-args['--listen'] = args['--listen'] || [];
-
-if (args['--port']) {
- const {isNaN} = Number;
- const port = Number(args['--port']);
- if (isNaN(port) || (!isNaN(port) && (port < 1 || port >= Math.pow(2, 16)))) {
- logError(
- `Port option must be a number. Supplied: ${args['--port']}`,
- 'invalid-server-port'
- );
- process.exit(1);
- }
-
- deprecatedEndpoint = [args['--port']];
-}
-
-if (args['--host']) {
- deprecatedEndpoint = deprecatedEndpoint || [];
- deprecatedEndpoint.push(args['--host']);
-}
-
-if (deprecatedEndpoint) {
- args['--listen'].push(deprecatedEndpoint);
-}
-
-if (args['--unix-socket']) {
- if (typeof args['--unix-socket'] === 'boolean') {
- logError(
- `Socket must be a string. A boolean was provided.`,
- 'invalid-socket'
- );
- }
- args['--listen'].push(args['--unix-socket']);
-}
-
-if (args['--port'] || args['--host'] || args['--unix-socket']) {
- logError(
- '--port, --host, and --unix-socket are deprecated - see --help for information on the --listen flag',
- 'deprecated-endpoint-flags'
- );
-}
-
-if (args['--listen'].length === 0) {
- // default endpoint
- args['--listen'].push([3000]);
-}
-
-let file = args._[0];
-
-if (!file) {
- try {
- const packageJson = require(path.resolve(process.cwd(), 'package.json'));
- file = packageJson.main || 'index.js';
- } catch (err) {
- if (err.code !== 'MODULE_NOT_FOUND') {
- logError(
- `Could not read \`package.json\`: ${err.message}`,
- 'invalid-package-json'
- );
- process.exit(1);
- }
- }
-}
-
-if (!file) {
- logError('Please supply a file!', 'path-missing');
- process.exit(1);
-}
-
-if (file[0] !== '/') {
- file = path.resolve(process.cwd(), file);
-}
-
-if (!existsSync(file)) {
- logError(
- `The file or directory "${path.basename(file)}" doesn't exist!`,
- 'path-not-existent'
- );
- process.exit(1);
-}
-
-function registerShutdown(fn) {
- let run = false;
-
- const wrapper = () => {
- if (!run) {
- run = true;
- fn();
- }
- };
-
- process.on('SIGINT', wrapper);
- process.on('SIGTERM', wrapper);
- process.on('exit', wrapper);
-}
-
-function startEndpoint(module, endpoint) {
- const server = new http.Server(serve(module));
-
- server.on('error', err => {
- console.error('micro:', err.stack);
- process.exit(1);
- });
-
- server.listen(...endpoint, () => {
- const details = server.address();
- registerShutdown(() => {
- console.log('micro: Gracefully shutting down. Please wait...');
- server.close();
- process.exit();
- });
-
- // `micro` is designed to run only in production, so
- // this message is perfectly for prod
- if (typeof details === 'string') {
- console.log(`micro: Accepting connections on ${details}`);
- } else if (typeof details === 'object' && details.port) {
- console.log(`micro: Accepting connections on port ${details.port}`);
- } else {
- console.log('micro: Accepting connections');
- }
- });
-}
-
-async function start() {
- const loadedModule = await handle(file);
-
- for (const endpoint of args['--listen']) {
- startEndpoint(loadedModule, endpoint);
- }
-}
-
-start();
diff --git a/packages/micro/lib/error.js b/packages/micro/lib/error.js
deleted file mode 100644
index 38fa72ff..00000000
--- a/packages/micro/lib/error.js
+++ /dev/null
@@ -1,4 +0,0 @@
-module.exports = (message, errorCode) => {
- console.error(`micro: ${message}`);
- console.error(`micro: https://err.sh/micro/${errorCode}`);
-};
diff --git a/packages/micro/lib/handler.js b/packages/micro/lib/handler.js
deleted file mode 100644
index 4b8fd1c1..00000000
--- a/packages/micro/lib/handler.js
+++ /dev/null
@@ -1,24 +0,0 @@
-// Utilities
-const logError = require('./error');
-
-module.exports = async file => {
- let mod;
-
- try {
- mod = await require(file); // Await to support exporting Promises
-
- if (mod && typeof mod === 'object') {
- mod = await mod.default; // Await to support es6 module's default export
- }
- } catch (err) {
- 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;
-};
diff --git a/packages/micro/lib/index.js b/packages/micro/lib/index.js
deleted file mode 100644
index 7515d091..00000000
--- a/packages/micro/lib/index.js
+++ /dev/null
@@ -1,169 +0,0 @@
-// Native
-const http = require('http');
-const {Stream} = require('stream');
-
-// Packages
-const contentType = require('content-type');
-const getRawBody = require('raw-body');
-
-// based on is-stream https://github.com/sindresorhus/is-stream/blob/c918e3795ea2451b5265f331a00fb6a8aaa27816/license
-function isStream(stream) {
- return stream !== null &&
- typeof stream === 'object' &&
- typeof stream.pipe === 'function';
-}
-
-function readable(stream) {
- return isStream(stream) &&
- stream.readable !== false &&
- typeof stream._read === 'function' &&
- typeof stream._readableState === 'object';
-}
-
-const {NODE_ENV} = process.env;
-const DEV = NODE_ENV === 'development';
-
-const serve = fn => new http.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;
- }
-
- if (obj instanceof Stream || readable(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; charset=utf-8');
- }
- }
-
- res.setHeader('Content-Length', Buffer.byteLength(str));
- res.end(str);
-};
-
-const sendError = (req, res, errorObj) => {
- const statusCode = errorObj.statusCode || errorObj.status;
- const message = statusCode ? errorObj.message : 'Internal Server Error';
- send(res, statusCode || 500, DEV ? errorObj.stack : message);
- if (errorObj instanceof Error) {
- console.error(errorObj.stack);
- } else {
- console.warn('thrown error must be an instance Error');
- }
-};
-
-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 (val !== undefined) {
- 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'];
-
- if (encoding === undefined) {
- encoding = contentType.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));
diff --git a/packages/micro/lib/parse-endpoint.js b/packages/micro/lib/parse-endpoint.js
deleted file mode 100644
index 477c728d..00000000
--- a/packages/micro/lib/parse-endpoint.js
+++ /dev/null
@@ -1,26 +0,0 @@
-const {URL} = require('url');
-
-module.exports = function parseEndpoint(str) {
- const url = new URL(str);
-
- switch (url.protocol) {
- case 'pipe:': {
- // some special handling
- const cutStr = str.replace(/^pipe:/, '');
- if (cutStr.slice(0, 4) !== '\\\\.\\') {
- throw new Error(`Invalid Windows named pipe endpoint: ${str}`);
- }
- return [cutStr];
- }
- case 'unix:':
- if (!url.pathname) {
- throw new Error(`Invalid UNIX domain socket endpoint: ${str}`);
- }
- return [url.pathname];
- case 'tcp:':
- url.port = url.port || '3000';
- return [parseInt(url.port, 10), url.hostname];
- default:
- throw new Error(`Unknown --listen endpoint scheme (protocol): ${url.protocol}`);
- }
-};
diff --git a/packages/micro/micro.d.ts b/packages/micro/micro.d.ts
deleted file mode 100644
index 876d88ef..00000000
--- a/packages/micro/micro.d.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-///
-
-import { RequestListener, IncomingMessage, ServerResponse } from 'http'
-
-export type RequestHandler = (req: IncomingMessage, res: ServerResponse) => any
-declare function serve(fn: RequestHandler): RequestListener
-
-export function run(req: IncomingMessage, res: ServerResponse, fn: RequestHandler): Promise
-export function json(req: IncomingMessage, info?: { limit?: string | number, encoding?: string }): Promise
-export function text(req: IncomingMessage, info?: { limit?: string | number, encoding?: string }): Promise
-export function buffer(req: IncomingMessage, info?: { limit?: string | number, encoding?: string }): Promise
-export function send(res: ServerResponse, code: number, data?: any): Promise
-export function createError(code: number, msg: string, orig?: Error): Error & { statusCode: number, originalError?: Error }
-export function sendError(req: IncomingMessage, res: ServerResponse, info: { statusCode?: number, status?: number, message?: string, stack?: string }): Promise
-export default serve
diff --git a/packages/micro/package.json b/packages/micro/package.json
index e55195ff..f73c2889 100644
--- a/packages/micro/package.json
+++ b/packages/micro/package.json
@@ -3,18 +3,25 @@
"version": "9.4.1",
"description": "Asynchronous HTTP microservices",
"license": "MIT",
- "main": "./lib/index.js",
- "types": "./micro.d.ts",
+ "main": "./dist/src/lib/index.js",
+ "types": "./types",
"files": [
- "bin",
- "lib",
- "micro.d.ts"
+ "src",
+ "dist",
+ "types"
],
"bin": {
- "micro": "./bin/micro.js"
+ "micro": "./dist/src/bin/micro.js"
},
"engines": {
- "node": ">= 8.0.0"
+ "node": ">= 14.5.0"
+ },
+ "scripts": {
+ "build": "tsc",
+ "prepublishOnly": "yarn run build",
+ "eslint-check": "eslint --max-warnings=0 .",
+ "prettier-check": "prettier --check .",
+ "type-check": "tsc --noEmit"
},
"repository": "vercel/micro",
"keywords": [
@@ -27,6 +34,16 @@
"dependencies": {
"arg": "4.1.0",
"content-type": "1.0.4",
- "raw-body": "2.4.1"
- }
+ "raw-body": "2.4.1",
+ "tsimportlib": "0.0.3"
+ },
+ "devDependencies": {
+ "@types/content-type": "1.1.5",
+ "@types/node": "18.0.3",
+ "@vercel/style-guide": "3.0.0",
+ "eslint": "8.19.0",
+ "prettier": "2.7.1",
+ "typescript": "4.7.4"
+ },
+ "prettier": "@vercel/style-guide/prettier"
}
diff --git a/packages/micro/src/bin/micro.ts b/packages/micro/src/bin/micro.ts
new file mode 100755
index 00000000..0580d949
--- /dev/null
+++ b/packages/micro/src/bin/micro.ts
@@ -0,0 +1,208 @@
+#!/usr/bin/env node
+/* eslint-disable eslint-comments/disable-enable-pair */
+/* eslint-disable no-console */
+
+// Native
+import Module from 'module';
+import http from 'http';
+import path from 'path';
+import { existsSync } from 'fs';
+// Packages
+import arg from 'arg';
+// Utilities
+import { serve } from '../lib';
+import { handle } from '../lib/handler';
+import { version } from '../../package.json';
+import { logError } from '../lib/error';
+import { parseEndpoint } from '../lib/parse-endpoint';
+import type { AddressInfo } from 'net';
+import type { RequestHandler } from '../lib';
+
+// Check if the user defined any options
+const args = arg({
+ '--listen': parseEndpoint,
+ '-l': '--listen',
+ '--help': Boolean,
+ '--version': Boolean,
+ '-v': '--version',
+});
+
+// When `-h` or `--help` are used, print out
+// the usage information
+if (args['--help']) {
+ console.error(`
+ micro - Asynchronous HTTP microservices
+
+ USAGE
+
+ $ micro --help
+ $ micro --version
+ $ micro [-l listen_uri [-l ...]] [entry_point.js]
+
+ By default micro will listen on 0.0.0.0:3000 and will look first
+ for the "main" property in package.json and subsequently for index.js
+ as the default entry_point.
+
+ Specifying a single --listen argument will overwrite the default, not supplement it.
+
+ OPTIONS
+
+ --help shows this help message
+
+ -v, --version displays the current version of micro
+
+ -l, --listen listen_uri specify a URI endpoint on which to listen (see below) -
+ more than one may be specified to listen in multiple places
+
+ ENDPOINTS
+
+ Listen endpoints (specified by the --listen or -l options above) instruct micro
+ to listen on one or more interfaces/ports, UNIX domain sockets, or Windows named pipes.
+
+ For TCP (traditional host/port) endpoints:
+
+ $ micro -l tcp://hostname:1234
+
+ For UNIX domain socket endpoints:
+
+ $ micro -l unix:/path/to/socket.sock
+
+ For Windows named pipe endpoints:
+
+ $ micro -l pipe:\\\\.\\pipe\\PipeName
+`);
+ process.exit(2);
+}
+
+// Print out the package's version when
+// `--version` or `-v` are used
+if (args['--version']) {
+ console.log(version);
+ process.exit();
+}
+
+if (!args['--listen']) {
+ // default endpoint
+ args['--listen'] = [String(3000)];
+}
+
+let file = args._[0];
+
+if (!file) {
+ try {
+ const req = Module.createRequire(module.filename);
+ const packageJson: unknown = req(
+ path.resolve(process.cwd(), 'package.json'),
+ );
+ if (hasMain(packageJson)) {
+ file = packageJson.main;
+ } else {
+ file = 'index.js';
+ }
+ } catch (err) {
+ if (isNodeError(err) && err.code !== 'MODULE_NOT_FOUND') {
+ logError(
+ `Could not read \`package.json\`: ${err.message}`,
+ 'invalid-package-json',
+ );
+ process.exit(1);
+ }
+ }
+}
+
+if (!file) {
+ logError('Please supply a file!', 'path-missing');
+ process.exit(1);
+}
+
+if (!file.startsWith('/')) {
+ file = path.resolve(process.cwd(), file);
+}
+
+if (!existsSync(file)) {
+ logError(
+ `The file or directory "${path.basename(file)}" doesn't exist!`,
+ 'path-not-existent',
+ );
+ process.exit(1);
+}
+
+function registerShutdown(fn: () => void) {
+ let run = false;
+
+ const wrapper = () => {
+ if (!run) {
+ run = true;
+ fn();
+ }
+ };
+
+ process.on('SIGINT', wrapper);
+ process.on('SIGTERM', wrapper);
+ process.on('exit', wrapper);
+}
+
+function startEndpoint(module: RequestHandler, endpoint: string) {
+ const server = new http.Server(serve(module));
+
+ server.on('error', (err) => {
+ console.error('micro:', err.stack);
+ process.exit(1);
+ });
+
+ server.listen(endpoint, () => {
+ const details = server.address();
+ registerShutdown(() => {
+ console.log('micro: Gracefully shutting down. Please wait...');
+ server.close();
+ process.exit();
+ });
+
+ // `micro` is designed to run only in production, so
+ // this message is perfect for prod
+ if (typeof details === 'string') {
+ console.log(`micro: Accepting connections on ${details}`);
+ } else if (isAddressInfo(details)) {
+ console.log(`micro: Accepting connections on port ${details.port}`);
+ } else {
+ console.log('micro: Accepting connections');
+ }
+ });
+}
+
+async function start() {
+ if (file && args['--listen']) {
+ const loadedModule = await handle(file);
+
+ for (const endpoint of args['--listen']) {
+ startEndpoint(loadedModule as RequestHandler, endpoint);
+ }
+ }
+}
+
+start()
+ .then()
+ .catch((error) => {
+ if (error instanceof Error) {
+ logError(error.message, 'STARTUP_FAILURE');
+ }
+ process.exit(1);
+ });
+
+function hasMain(packageJson: unknown): packageJson is { main: string } {
+ return (
+ typeof packageJson === 'object' &&
+ packageJson !== null &&
+ 'main' in packageJson
+ );
+}
+
+function isNodeError(
+ error: unknown,
+): error is { code: string; message: string } {
+ return error instanceof Error && 'code' in error;
+}
+
+function isAddressInfo(obj: unknown): obj is AddressInfo {
+ return 'port' in (obj as AddressInfo);
+}
diff --git a/packages/micro/src/lib/error.ts b/packages/micro/src/lib/error.ts
new file mode 100644
index 00000000..fd9d63ac
--- /dev/null
+++ b/packages/micro/src/lib/error.ts
@@ -0,0 +1,6 @@
+// eslint-disable-next-line eslint-comments/disable-enable-pair
+/* eslint-disable no-console */
+export function logError(message: string, errorCode: string) {
+ console.error(`micro: ${message}`);
+ console.error(`micro: https://err.sh/micro/${errorCode}`);
+}
diff --git a/packages/micro/src/lib/handler.ts b/packages/micro/src/lib/handler.ts
new file mode 100644
index 00000000..ed244a8e
--- /dev/null
+++ b/packages/micro/src/lib/handler.ts
@@ -0,0 +1,30 @@
+// Utilities
+import { logError } from './error';
+
+export const handle = async (file: string) => {
+ let mod: unknown;
+
+ try {
+ mod = await import(file);
+
+ if (mod && typeof mod === 'object') {
+ mod = await (mod as { default: unknown }).default; // Await to support es6 module's default export
+ }
+ } catch (err: unknown) {
+ if (isErrorObject(err) && err.stack) {
+ 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;
+};
+
+function isErrorObject(error: unknown): error is Error {
+ return (error as Error).stack !== undefined;
+}
diff --git a/packages/micro/src/lib/index.ts b/packages/micro/src/lib/index.ts
new file mode 100644
index 00000000..8977b35e
--- /dev/null
+++ b/packages/micro/src/lib/index.ts
@@ -0,0 +1,215 @@
+// Native
+import { Stream, Readable } from 'stream';
+// Packages
+import contentType from 'content-type';
+import getRawBody from 'raw-body';
+import type { RawBodyError } from 'raw-body';
+//Types
+import type { IncomingMessage, ServerResponse, RequestListener } from 'http';
+
+// slight modification of is-stream https://github.com/sindresorhus/is-stream/blob/c918e3795ea2451b5265f331a00fb6a8aaa27816/license
+function isStream(stream: unknown): stream is Stream {
+ return (
+ stream !== null &&
+ typeof stream === 'object' &&
+ stream instanceof Stream &&
+ typeof stream.pipe === 'function'
+ );
+}
+
+function readable(stream: unknown): stream is Readable {
+ return (
+ isStream(stream) && // TODO: maybe this isn't needed because we could use only the checks below
+ stream instanceof Readable &&
+ stream.readable
+ );
+}
+
+export type RequestHandler = (
+ req: IncomingMessage,
+ res: ServerResponse,
+) => unknown;
+
+type Serve = (fn: RequestHandler) => RequestListener;
+
+export const serve: Serve = (fn) => (req, res) => run(req, res, fn);
+
+export class HttpError extends Error {
+ constructor(message: string) {
+ super(message);
+ Object.setPrototypeOf(this, HttpError.prototype);
+ }
+
+ statusCode?: number;
+ originalError?: Error;
+}
+
+function isError(error: unknown): error is Error | HttpError {
+ return error instanceof Error || error instanceof HttpError;
+}
+
+export const createError = (code: number, message: string, original: Error) => {
+ const err = new HttpError(message);
+
+ err.statusCode = code;
+ err.originalError = original;
+
+ return err;
+};
+
+export const send = (
+ res: ServerResponse,
+ code: number,
+ obj: unknown = 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 (obj instanceof Stream || readable(obj)) {
+ //TODO: Wouldn't (obj instanceof Stream) be the only check here? Do we specifically need a Readable stream or a Stream object that's not of NodeJS Stream?
+ 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
+ str = JSON.stringify(obj);
+
+ if (!res.getHeader('Content-Type')) {
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
+ }
+ }
+
+ if (typeof str === 'string') {
+ res.setHeader('Content-Length', Buffer.byteLength(str));
+ }
+
+ res.end(str);
+};
+
+export const sendError = (
+ req: IncomingMessage,
+ res: ServerResponse,
+ errorObj: Error | HttpError,
+) => {
+ if ('statusCode' in errorObj && errorObj.statusCode) {
+ send(res, errorObj.statusCode, errorObj.message);
+ } else send(res, 500, 'Internal Server Error');
+
+ if (errorObj instanceof Error) {
+ // eslint-disable-next-line no-console
+ console.error(errorObj.stack);
+ } else {
+ // eslint-disable-next-line no-console
+ console.warn('thrown error must be an instance Error');
+ }
+};
+
+export const run = (
+ req: IncomingMessage,
+ res: ServerResponse,
+ fn: RequestHandler,
+) =>
+ 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 (val !== undefined) {
+ send(res, res.statusCode || 200, val);
+ }
+ })
+ .catch((err: unknown) => {
+ if (isError(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: string): unknown => {
+ try {
+ return JSON.parse(str);
+ } catch (err: unknown) {
+ throw createError(400, 'Invalid JSON', err as Error);
+ }
+};
+
+export interface BufferInfo {
+ limit?: string | number | undefined;
+ encoding?: BufferEncoding;
+}
+
+function isRawBodyError(error: unknown): error is RawBodyError {
+ return 'type' in (error as RawBodyError);
+}
+
+export const buffer = (
+ req: IncomingMessage,
+ { limit = '1mb', encoding }: BufferInfo = {},
+) =>
+ Promise.resolve().then(() => {
+ const type = req.headers['content-type'] || 'text/plain';
+ const length = req.headers['content-length'];
+
+ const body = rawBodyMap.get(req);
+
+ if (body) {
+ return body;
+ }
+
+ return getRawBody(req, {
+ limit,
+ length,
+ encoding: encoding ?? contentType.parse(type).parameters.charset,
+ })
+ .then((buf) => {
+ rawBodyMap.set(req, buf);
+ return buf;
+ })
+ .catch((err) => {
+ if (isRawBodyError(err) && err.type === 'entity.too.large') {
+ throw createError(413, `Body exceeded ${limit} limit`, err);
+ } else {
+ throw createError(400, 'Invalid body', err as Error);
+ }
+ });
+ });
+
+export const text = (
+ req: IncomingMessage,
+ { limit, encoding }: BufferInfo = {},
+) => buffer(req, { limit, encoding }).then((body) => body.toString(encoding));
+
+export const json = (req: IncomingMessage, opts: BufferInfo = {}) =>
+ text(req, opts).then((body) => parseJSON(body));
diff --git a/packages/micro/src/lib/parse-endpoint.ts b/packages/micro/src/lib/parse-endpoint.ts
new file mode 100644
index 00000000..03b73d3a
--- /dev/null
+++ b/packages/micro/src/lib/parse-endpoint.ts
@@ -0,0 +1,26 @@
+export function parseEndpoint(endpoint: string) {
+ const url = new URL(endpoint);
+
+ switch (url.protocol) {
+ case 'pipe:': {
+ // some special handling
+ const cutStr = endpoint.replace(/^pipe:/, '');
+ if (!cutStr.startsWith('\\\\.\\')) {
+ throw new Error(`Invalid Windows named pipe endpoint: ${endpoint}`);
+ }
+ return [cutStr];
+ }
+ case 'unix:':
+ if (!url.pathname) {
+ throw new Error(`Invalid UNIX domain socket endpoint: ${endpoint}`);
+ }
+ return [url.pathname];
+ case 'tcp:':
+ url.port = url.port || '3000';
+ return [parseInt(url.port, 10).toString(), url.hostname];
+ default:
+ throw new Error(
+ `Unknown --listen endpoint scheme (protocol): ${url.protocol}`,
+ );
+ }
+}
diff --git a/packages/micro/tsconfig.json b/packages/micro/tsconfig.json
new file mode 100644
index 00000000..4265e9dd
--- /dev/null
+++ b/packages/micro/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "@vercel/style-guide/typescript",
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "CommonJS",
+ "moduleResolution": "node",
+ "esModuleInterop": true,
+ "resolveJsonModule": true,
+ "outDir": "dist",
+ "declaration": true,
+ "declarationDir": "./types",
+ "declarationMap": true,
+ "removeComments": true
+ },
+ "include": ["src"]
+}
diff --git a/packages/micro/types/src/bin/micro.d.ts b/packages/micro/types/src/bin/micro.d.ts
new file mode 100644
index 00000000..b2a4eef9
--- /dev/null
+++ b/packages/micro/types/src/bin/micro.d.ts
@@ -0,0 +1,3 @@
+#!/usr/bin/env node
+export {};
+//# sourceMappingURL=micro.d.ts.map
\ No newline at end of file
diff --git a/packages/micro/types/src/bin/micro.d.ts.map b/packages/micro/types/src/bin/micro.d.ts.map
new file mode 100644
index 00000000..2455f9cf
--- /dev/null
+++ b/packages/micro/types/src/bin/micro.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"micro.d.ts","sourceRoot":"","sources":["../../../src/bin/micro.ts"],"names":[],"mappings":""}
\ No newline at end of file
diff --git a/packages/micro/types/src/lib/error.d.ts b/packages/micro/types/src/lib/error.d.ts
new file mode 100644
index 00000000..45682178
--- /dev/null
+++ b/packages/micro/types/src/lib/error.d.ts
@@ -0,0 +1,2 @@
+export declare function logError(message: string, errorCode: string): void;
+//# sourceMappingURL=error.d.ts.map
\ No newline at end of file
diff --git a/packages/micro/types/src/lib/error.d.ts.map b/packages/micro/types/src/lib/error.d.ts.map
new file mode 100644
index 00000000..41a3dc82
--- /dev/null
+++ b/packages/micro/types/src/lib/error.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../../src/lib/error.ts"],"names":[],"mappings":"AAEA,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,QAG1D"}
\ No newline at end of file
diff --git a/packages/micro/types/src/lib/handler.d.ts b/packages/micro/types/src/lib/handler.d.ts
new file mode 100644
index 00000000..156a309f
--- /dev/null
+++ b/packages/micro/types/src/lib/handler.d.ts
@@ -0,0 +1,2 @@
+export declare const handle: (file: string) => Promise;
+//# sourceMappingURL=handler.d.ts.map
\ No newline at end of file
diff --git a/packages/micro/types/src/lib/handler.d.ts.map b/packages/micro/types/src/lib/handler.d.ts.map
new file mode 100644
index 00000000..2c97cb80
--- /dev/null
+++ b/packages/micro/types/src/lib/handler.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/lib/handler.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,MAAM,SAAgB,MAAM,sBAsBxC,CAAC"}
\ No newline at end of file
diff --git a/packages/micro/types/src/lib/index.d.ts b/packages/micro/types/src/lib/index.d.ts
new file mode 100644
index 00000000..fd7ece72
--- /dev/null
+++ b/packages/micro/types/src/lib/index.d.ts
@@ -0,0 +1,24 @@
+///
+///
+import type { IncomingMessage, ServerResponse, RequestListener } from 'http';
+export declare type RequestHandler = (req: IncomingMessage, res: ServerResponse) => unknown;
+declare type Serve = (fn: RequestHandler) => RequestListener;
+export declare const serve: Serve;
+export declare class HttpError extends Error {
+ constructor(message: string);
+ statusCode?: number;
+ originalError?: Error;
+}
+export declare const createError: (code: number, message: string, original: Error) => HttpError;
+export declare const send: (res: ServerResponse, code: number, obj?: unknown) => void;
+export declare const sendError: (req: IncomingMessage, res: ServerResponse, errorObj: Error | HttpError) => void;
+export declare const run: (req: IncomingMessage, res: ServerResponse, fn: RequestHandler) => Promise;
+export interface BufferInfo {
+ limit?: string | number | undefined;
+ encoding?: BufferEncoding;
+}
+export declare const buffer: (req: IncomingMessage, { limit, encoding }?: BufferInfo) => Promise;
+export declare const text: (req: IncomingMessage, { limit, encoding }?: BufferInfo) => Promise;
+export declare const json: (req: IncomingMessage, opts?: BufferInfo) => Promise;
+export {};
+//# sourceMappingURL=index.d.ts.map
\ No newline at end of file
diff --git a/packages/micro/types/src/lib/index.d.ts.map b/packages/micro/types/src/lib/index.d.ts.map
new file mode 100644
index 00000000..4fcdc0b7
--- /dev/null
+++ b/packages/micro/types/src/lib/index.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/index.ts"],"names":[],"mappings":";;AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAoB7E,oBAAY,cAAc,GAAG,CAC3B,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,KAChB,OAAO,CAAC;AAEb,aAAK,KAAK,GAAG,CAAC,EAAE,EAAE,cAAc,KAAK,eAAe,CAAC;AAErD,eAAO,MAAM,KAAK,EAAE,KAA+C,CAAC;AAEpE,qBAAa,SAAU,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM;IAK3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,KAAK,CAAC;CACvB;AAMD,eAAO,MAAM,WAAW,SAAU,MAAM,WAAW,MAAM,YAAY,KAAK,cAOzE,CAAC;AAEF,eAAO,MAAM,IAAI,QACV,cAAc,QACb,MAAM,QACP,OAAO,SA+Cb,CAAC;AAEF,eAAO,MAAM,SAAS,QACf,eAAe,OACf,cAAc,YACT,KAAK,GAAG,SAAS,SAa5B,CAAC;AAEF,eAAO,MAAM,GAAG,QACT,eAAe,OACf,cAAc,MACf,cAAc,kBAqBd,CAAC;AAcP,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAMD,eAAO,MAAM,MAAM,QACZ,eAAe,wBACS,UAAU,oBA4BrC,CAAC;AAEL,eAAO,MAAM,IAAI,QACV,eAAe,wBACC,UAAU,oBAC4C,CAAC;AAE9E,eAAO,MAAM,IAAI,QAAS,eAAe,SAAQ,UAAU,qBACV,CAAC"}
\ No newline at end of file
diff --git a/packages/micro/types/src/lib/parse-endpoint.d.ts b/packages/micro/types/src/lib/parse-endpoint.d.ts
new file mode 100644
index 00000000..43d18f17
--- /dev/null
+++ b/packages/micro/types/src/lib/parse-endpoint.d.ts
@@ -0,0 +1,2 @@
+export declare function parseEndpoint(endpoint: string): string[];
+//# sourceMappingURL=parse-endpoint.d.ts.map
\ No newline at end of file
diff --git a/packages/micro/types/src/lib/parse-endpoint.d.ts.map b/packages/micro/types/src/lib/parse-endpoint.d.ts.map
new file mode 100644
index 00000000..9d34d5b2
--- /dev/null
+++ b/packages/micro/types/src/lib/parse-endpoint.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"parse-endpoint.d.ts","sourceRoot":"","sources":["../../../src/lib/parse-endpoint.ts"],"names":[],"mappings":"AAAA,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,YAyB7C"}
\ No newline at end of file
diff --git a/test/.eslintrc.js b/test/.eslintrc.js
new file mode 100644
index 00000000..4d1de0f6
--- /dev/null
+++ b/test/.eslintrc.js
@@ -0,0 +1,11 @@
+module.exports = {
+ root: true,
+ extends: [
+ require.resolve('@vercel/style-guide/eslint/node'),
+ require.resolve('@vercel/style-guide/eslint/typescript'),
+ ],
+ parserOptions: {
+ tsconfigRootDir: __dirname,
+ project: ['./tsconfig.json'],
+ },
+};
diff --git a/test/_test-utils.js b/test/_test-utils.js
deleted file mode 100644
index c0714ba3..00000000
--- a/test/_test-utils.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = ({ micro, listen }) => ({
- getUrl: (fn) => listen(micro(fn)),
-});
diff --git a/test/development.js b/test/development.js
deleted file mode 100644
index 03062902..00000000
--- a/test/development.js
+++ /dev/null
@@ -1,43 +0,0 @@
-// Packages
-const test = require('ava');
-const fetch = require('node-fetch');
-const listen = require('test-listen');
-
-process.env.NODE_ENV = 'development';
-const micro = require('../packages/micro/lib');
-
-const {getUrl} = require('./_test-utils')({micro, listen});
-
-test('send(200,