serve-handler
Advanced tools
Comparing version 1.0.0 to 1.1.0
132
lib/index.js
@@ -8,8 +8,11 @@ // Native | ||
const slasher = require('glob-slasher'); | ||
const minimatch = require('minimatch'); | ||
const pathToRegExp = require('path-to-regexp'); | ||
const mime = require('mime/lite'); | ||
const getHandlers = methods => { | ||
const {createReadStream} = fs; | ||
const {stat, createReadStream} = fs; | ||
return Object.assign({ | ||
stat, | ||
createReadStream | ||
@@ -19,9 +22,49 @@ }, methods); | ||
const toRegExp = (location, keys = null) => { | ||
const normalized = slasher(location).replace('*', '(.*)'); | ||
return pathToRegExp(normalized, keys); | ||
const sourceMatches = (source, requestPath, allowSegments) => { | ||
const keys = []; | ||
const slashed = slasher(source); | ||
let results = null; | ||
if (allowSegments) { | ||
const normalized = slashed.replace('*', '(.*)'); | ||
const expression = pathToRegExp(normalized, keys); | ||
results = expression.exec(requestPath); | ||
} | ||
if (results || minimatch(requestPath, slashed)) { | ||
return { | ||
keys, | ||
results | ||
}; | ||
} | ||
return null; | ||
}; | ||
const applyRewrites = (requestPath, rewrites) => { | ||
if (!Array.isArray(rewrites)) { | ||
const toTarget = (source, destination, previousPath) => { | ||
const matches = sourceMatches(source, previousPath, true); | ||
if (!matches) { | ||
return null; | ||
} | ||
const {keys, results} = matches; | ||
const props = {}; | ||
const {protocol} = url.parse(destination); | ||
const normalizedDest = protocol ? destination : slasher(destination); | ||
const toPath = pathToRegExp.compile(normalizedDest); | ||
for (let index = 0; index < keys.length; index++) { | ||
const {name} = keys[index]; | ||
props[name] = results[index + 1]; | ||
} | ||
return toPath(props); | ||
}; | ||
const applyRewrites = (requestPath, rewrites = []) => { | ||
if (rewrites.length === 0) { | ||
return requestPath; | ||
@@ -32,5 +75,6 @@ } | ||
const {source, destination} = rewrites[index]; | ||
const target = toTarget(source, destination, requestPath); | ||
if (toRegExp(source).test(requestPath)) { | ||
return applyRewrites(slasher(destination), rewrites); | ||
if (target) { | ||
return applyRewrites(slasher(target), rewrites); | ||
} | ||
@@ -42,4 +86,4 @@ } | ||
const applyRedirect = (rewrittenURL, redirects) => { | ||
if (!Array.isArray(redirects)) { | ||
const shouldRedirect = (rewrittenURL, redirects = []) => { | ||
if (redirects.length === 0) { | ||
return null; | ||
@@ -51,21 +95,8 @@ } | ||
for (let index = 0; index < redirects.length; index++) { | ||
const {destination, source, statusCode} = redirects[index]; | ||
const {source, destination, statusCode} = redirects[index]; | ||
const target = toTarget(source, destination, rewrittenURL); | ||
const keys = []; | ||
const expression = toRegExp(source, keys); | ||
const results = expression.exec(rewrittenURL); | ||
if (results) { | ||
const props = {}; | ||
const {protocol} = url.parse(destination); | ||
const normalizedDest = protocol ? destination : slasher(destination); | ||
const toPath = toRegExp.compile(normalizedDest); | ||
for (let i = 0; i < keys.length; i++) { | ||
const {name} = keys[i]; | ||
props[name] = results[index + 1]; | ||
} | ||
if (target) { | ||
return { | ||
target: toPath(props), | ||
target, | ||
statusCode: statusCode || 301 | ||
@@ -79,5 +110,39 @@ }; | ||
module.exports = async (request, response, config = {}, methods) => { | ||
const appendHeaders = (target, source) => { | ||
for (let index = 0; index < source.length; index++) { | ||
const {key, value} = source[index]; | ||
target[key] = value; | ||
} | ||
}; | ||
const getHeaders = async (handlers, customHeaders = [], {relative, absolute}) => { | ||
const related = {}; | ||
if (customHeaders.length > 0) { | ||
// By iterating over all headers and never stopping, developers | ||
// can specify multiple header sources in the config that | ||
// might match a single path. | ||
for (let index = 0; index < customHeaders.length; index++) { | ||
const {source, headers} = customHeaders[index]; | ||
if (sourceMatches(source, relative)) { | ||
appendHeaders(related, headers); | ||
} | ||
} | ||
} | ||
const stats = await handlers.stat(absolute); | ||
const defaultHeaders = { | ||
'Content-Type': mime.getType(relative), | ||
'Last-Modified': stats.mtime.toUTCString(), | ||
'Content-Length': stats.size | ||
}; | ||
return Object.assign(defaultHeaders, related); | ||
}; | ||
module.exports = async (request, response, config = {}, methods = {}) => { | ||
const cwd = process.cwd(); | ||
const current = config.path ? path.join(cwd, config.path) : cwd; | ||
const current = config.public ? path.join(cwd, config.public) : cwd; | ||
const handlers = getHandlers(methods); | ||
@@ -87,3 +152,3 @@ | ||
const rewrittenURL = applyRewrites(pathname, config.rewrites); | ||
const redirect = applyRedirect(rewrittenURL, config.redirects); | ||
const redirect = shouldRedirect(rewrittenURL, config.redirects); | ||
@@ -102,3 +167,10 @@ if (redirect) { | ||
if (relatedExists) { | ||
const headers = await getHeaders(handlers, config.headers, { | ||
relative: rewrittenURL, | ||
absolute: related | ||
}); | ||
response.writeHead(200, headers); | ||
handlers.createReadStream(related).pipe(response); | ||
return; | ||
@@ -105,0 +177,0 @@ } |
{ | ||
"name": "serve-handler", | ||
"version": "1.0.0", | ||
"version": "1.1.0", | ||
"description": "The routing foundation of `serve` and static deployments on Now", | ||
@@ -36,4 +36,6 @@ "main": "lib/index.js", | ||
"glob-slasher": "1.0.1", | ||
"mime": "2.3.1", | ||
"minimatch": "3.0.4", | ||
"path-to-regexp": "2.2.1" | ||
} | ||
} |
159
README.md
@@ -30,3 +30,3 @@ # serve-handler | ||
module.exports = async (request, response) => { | ||
await handler(request, response); | ||
await handler(request, response); | ||
}; | ||
@@ -37,13 +37,9 @@ ``` | ||
### Configuration | ||
## Options | ||
In order to allow for customizing the package's default behaviour, we implemented two more arguments for the function call. They are both to be seen as configuration arguments. | ||
If you want to customize the package's default behaviour, you can use the third argument of the function call to pass any of the configuration options listed below. Here's an example: | ||
#### Options | ||
The first one is for statically defined options: | ||
```js | ||
await handler(request, response, { | ||
path: 'dist' | ||
cleanUrls: true | ||
}); | ||
@@ -54,14 +50,147 @@ ``` | ||
| Name | Description | Default Value | | ||
|--------|--------------------------------------------------------------------|-----------------| | ||
| `path` | A custom directory to which all requested paths should be relative | `process.cwd()` | | ||
- [public](#public-boolean) (set sub directory to serve) | ||
- [cleanUrls](#cleanurls-booleanarray) (strip `.html` and `.htm` from paths) | ||
- [rewrites](#rewrites-array) (rewrite paths to different paths) | ||
- [redirects](#redirects-array) (forward paths to different paths or URLs) | ||
- [headers](#headers-array) (set custom headers) | ||
- [trailingSlash](#trailingslash-boolean) (remove or add trailing slashes to all paths) | ||
#### Middleware | ||
### public (Boolean) | ||
While the second one is for passing custom methods to replace the ones used in the package: | ||
By default, the current working directory will be served. If you only want to serve a specific path, you can use this options to pass a custom directory to be served relative to the current working directory. | ||
For example, if serving a [Jekyll](https://jekyllrb.com/) app, it would look like this: | ||
```json | ||
{ | ||
"public": "_site" | ||
} | ||
``` | ||
### cleanUrls (Boolean|Array) | ||
Assuming this is `true`, all `.html` and `.htm` files can be accessed without their extension (shown below). | ||
If one of these extensions is used at the end of a filename, it will automatically perform a redirect with status code [301](https://en.wikipedia.org/wiki/HTTP_301) to the same path, but with the extension dropped. | ||
```json | ||
{ | ||
"cleanUrls": true | ||
} | ||
``` | ||
However, you can also restrict this behavior to certain paths: | ||
```json | ||
{ | ||
"cleanUrls": [ | ||
"/app/**", | ||
"/!components/**" | ||
] | ||
} | ||
``` | ||
### rewrites (Array) | ||
If you want your visitors to receive a response under a certain path, but actually serve a completely different one behind the curtains, this option is what you need. | ||
It's perfect for [single page applications](https://en.wikipedia.org/wiki/Single-page_application) (SPAs), for example: | ||
```json | ||
{ | ||
"rewrites": [ | ||
{ "source": "app/**", "destination": "/index.html" }, | ||
{ "source": "projects/*/edit", "destination": "/edit-project.html" } | ||
] | ||
} | ||
``` | ||
You can also use so-called "routing segments" as follows: | ||
```json | ||
{ | ||
"rewrites": [ | ||
{ "source": "/projects/:id/edit", "destination": "/edit-project-:id.html" }, | ||
] | ||
} | ||
``` | ||
Now, if a visitor accesses `/projects/123/edit`, it will respond with the file `/edit-project-123.html`. | ||
### redirects (Array) | ||
In order to redirect visits to a certain path to a different one (or even an external URL), you can use this option: | ||
```json | ||
{ | ||
"redirects": [ | ||
{ "source": "/from", "destination": "/to" }, | ||
{ "source": "/old-pages/**", "destination": "/home" } | ||
] | ||
} | ||
``` | ||
By default, all of them are performed with the status code [301](https://en.wikipedia.org/wiki/HTTP_301), but this behavior can be adjusted by setting the `type` property directly on the object (see below). | ||
Just like with [rewrites](#rewrites-array), you can also use routing segments: | ||
```json | ||
{ | ||
"redirects": [ | ||
{ "source": "/old-docs/:id", "destination": "/new-docs/:id" }, | ||
{ "source": "/old", "destination": "/new", "type": 302 } | ||
] | ||
} | ||
``` | ||
In the example above, `/old-docs/12` would be forwarded to `/new-docs/12` with status code [301](https://en.wikipedia.org/wiki/HTTP_301). In addition `/old` would be forwarded to `/new` with status code [302](https://en.wikipedia.org/wiki/HTTP_302). | ||
### headers (Array) | ||
Allows you to set custom headers (and overwrite the default ones) for certain paths: | ||
```json | ||
{ | ||
"headers": [ | ||
{ | ||
"source" : "**/*.@(jpg|jpeg|gif|png)", | ||
"headers" : [{ | ||
"key" : "Cache-Control", | ||
"value" : "max-age=7200" | ||
}] | ||
}, { | ||
"source" : "404.html", | ||
"headers" : [{ | ||
"key" : "Cache-Control", | ||
"value" : "max-age=300" | ||
}] | ||
}] | ||
} | ||
} | ||
``` | ||
### trailingSlash (Boolean) | ||
By default, the package will try to make assumptions for when to add trailing slashes to your URLs or not. If you want to remove them, set this property to `false` and `true` if you want to force them on all URLs: | ||
```js | ||
{ | ||
"trailingSlash": true | ||
} | ||
``` | ||
With the above config, a request to `/test` would now result in a [301](https://en.wikipedia.org/wiki/HTTP_301) redirect to `/test/`. | ||
## Middleware | ||
If you want to replace the methods the package is using for interacting with the file system, you can pass them as the fourth argument to the function call. | ||
This comes in handy if you're dealing with simulating a file system, for example. | ||
These are the methods used by the package (they can all return a `Promise` or be asynchronous): | ||
```js | ||
await handler(request, response, null, { | ||
createReadStream(path) {}, | ||
stat(path) {} | ||
createReadStream(path) {}, | ||
stat(path) {} | ||
}); | ||
@@ -68,0 +197,0 @@ ``` |
Sorry, the diff of this file is not supported yet
45163
134
213
6
+ Addedmime@2.3.1
+ Addedminimatch@3.0.4
+ Addedbalanced-match@1.0.2(transitive)
+ Addedbrace-expansion@1.1.11(transitive)
+ Addedconcat-map@0.0.1(transitive)
+ Addedmime@2.3.1(transitive)
+ Addedminimatch@3.0.4(transitive)