New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

bunshine

Package Overview
Dependencies
Maintainers
1
Versions
43
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

bunshine - npm Package Compare versions

Comparing version 0.11.2 to 0.12.0

.prettierrc.json

50

benchmarks/inner-functions.ts

@@ -1,3 +0,9 @@

import { bench, group, run } from 'mitata';
import { runBenchmarks } from './runBenchmarks.ts';
/*
Conclusion:
classes are
1.03x faster than inner functions
*/
class TheClass {

@@ -18,23 +24,23 @@ max: number;

// These 2 approaches perform within 1% to 3% of each other
group('2 functions', () => {
bench('inner functions', () => {
const spec = { max: 10000 };
function addUp() {
let sum = 0;
for (let i = 0; i < spec.max; i++) {
sum += i;
await runBenchmarks(
{
'inner functions': () => {
const spec = { max: 10000 };
function addUp() {
let sum = 0;
for (let i = 0; i < spec.max; i++) {
sum += i;
}
return sum;
}
return sum;
}
function noop() {}
addUp();
});
bench('class', () => {
const spec = { max: 10000 };
const theClass = new TheClass(spec);
theClass.addUp();
});
});
await run();
function noop() {}
addUp();
},
classes: () => {
const spec = { max: 10000 };
const theClass = new TheClass(spec);
theClass.addUp();
},
},
{ time: 15000 }
);

@@ -1,25 +0,31 @@

import { bench, group, run } from 'mitata';
import MatcherWithCache from '../src/MatcherWithCache/MatcherWithCache.ts';
import PathMatcher from '../src/PathMatcher/PathMatcher.ts';
import { runBenchmarks } from './runBenchmarks.ts';
group('lru cache speed', () => {
function withCacheSize(size: number) {
const matcher =
size === 0
? new PathMatcher()
: new MatcherWithCache(new PathMatcher(), size);
return setup(matcher);
}
// cache size of 5000 is best, averaging 40x faster than no cache
bench('no cache', withCacheSize(0));
bench('cache size 500', withCacheSize(500));
bench('cache size 2500', withCacheSize(2500));
bench('cache size 3000', withCacheSize(3000));
bench('cache size 4000', withCacheSize(4000));
bench('cache size 5000', withCacheSize(5000));
bench('cache size 10000', withCacheSize(8000));
});
/*
Conclusion:
Cache sizes of 4000+ are all about 41x faster than no cache
*/
await run();
function withCacheSize(size: number) {
const matcher =
size === 0
? new PathMatcher()
: new MatcherWithCache(new PathMatcher(), size);
return setup(matcher);
}
await runBenchmarks(
{
'no cache': withCacheSize(0),
'cache size 500': withCacheSize(500),
'cache size 2500': withCacheSize(2500),
'cache size 3000': withCacheSize(3000),
'cache size 4000': withCacheSize(4000),
'cache size 5000': withCacheSize(5000),
'cache size 10000': withCacheSize(10000),
},
{ time: 5000 }
);
function setup(matcher: any) {

@@ -26,0 +32,0 @@ const urls: string[] = [];

import { LRUCache } from 'lru-cache';
import { bench, group, run } from 'mitata';
import { match } from 'path-to-regexp';
import { runBenchmarks } from './runBenchmarks.ts';
/*
Conclusion:
Cache sizes of 5000+ are all about 40x faster than no cache
*/
const { findAll, getLruFinder, urls } = setup();
group('lru cache speed', () => {
function finder(find: (url: string) => void) {
return function () {
for (const url of urls) {
find(url);
}
};
}
// cache size of 5000 is best, averaging 40x faster than no cache
bench('no cache', finder(findAll));
bench('cache size 500', finder(getLruFinder(500)));
bench('cache size 5000', finder(getLruFinder(5000)));
bench('cache size 50000', finder(getLruFinder(50000)));
bench('cache size 500000', finder(getLruFinder(500000)));
});
function finder(find: (url: string) => void) {
return function () {
for (const url of urls) {
find(url);
}
};
}
// group('trie speed', () => { });
await runBenchmarks(
{
'no cache': finder(findAll),
'cache size 500': finder(getLruFinder(500)),
'cache size 5000': finder(getLruFinder(5000)),
'cache size 50000': finder(getLruFinder(50000)),
'cache size 500000': finder(getLruFinder(50000)),
},
{ time: 5000 }
);
await run();
function setup() {

@@ -41,3 +45,3 @@ const registry: Registration[] = [];

function findAll(urlPath: string) {
const found = [];
const found: Registration[] = [];
for (const reg of registry) {

@@ -57,3 +61,3 @@ if (reg.matcher(urlPath)) {

}
const found = [];
const found: Registration[] = [];
for (const reg of registry) {

@@ -60,0 +64,0 @@ if (reg.matcher(urlPath)) {

@@ -9,10 +9,3 @@ export { default as Context } from './src/Context/Context';

export {
file,
html,
js,
json,
redirect,
sse,
text,
xml,
type FileResponseOptions,

@@ -19,0 +12,0 @@ type SseSetupFunction,

{
"name": "bunshine",
"version": "0.11.2",
"version": "0.12.0",
"module": "server/server.ts",

@@ -11,2 +11,33 @@ "type": "module",

},
"repository": {
"type": "git",
"url": "git+https://github.com/kensnyder/bunshine.git"
},
"bin": {
"serve": "bun ./bin/serve.ts"
},
"keywords": [
"Bun HTTP Server",
"Bun Socket Server",
"Bun Server",
"Bun Framework",
"Bun Web Framework",
"Bun Server Sent Events",
"Bun Ranged Files",
"Bun File Server",
"HTTP Server",
"Socket Server",
"Server",
"Server Sent Events",
"Ranged Files",
"File Server",
"GZIP Responses",
"Bun"
],
"author": "kendsnyder@gmail.com",
"license": "ISC",
"bugs": {
"url": "https://github.com/kensnyder/bunshine/issues"
},
"homepage": "https://github.com/kensnyder/bunshine#readme",
"dependencies": {

@@ -20,7 +51,8 @@ "lru-cache": "^10.1.0",

"@types/ms": "^0.7.34",
"bun-types": "^1.0.22",
"bun-types": "^1.0.23",
"eventsource": "^2.0.2",
"mitata": "^0.1.6",
"prettier": "^3.1.1",
"globby": "^14.0.0",
"prettier": "^3.2.4",
"prettier-plugin-organize-imports": "^3.2.4",
"tinybench": "^2.6.0",
"type-fest": "^4.9.0",

@@ -27,0 +59,0 @@ "typescript": "^5.3.3"

@@ -1,7 +0,7 @@

<img alt="Bunshine Logo" src="https://github.com/kensnyder/bunshine/raw/main/assets/bunshine-logo.png?v=0.11.2" width="200" height="187" />
<img alt="Bunshine Logo" src="https://github.com/kensnyder/bunshine/raw/main/assets/bunshine-logo.png?v=0.12.0" width="200" height="187" />
[![NPM Link](https://img.shields.io/npm/v/bunshine?v=0.11.2)](https://npmjs.com/package/bunshine)
[![Dependencies](https://badgen.net/static/dependencies/3/green?v=0.11.2)](https://www.npmjs.com/package/bunshine?activeTab=dependencies)
![Test Coverage: 97%](https://badgen.net/static/test%20coverage/96%25/green?v=0.11.2)
[![ISC License](https://img.shields.io/npm/l/bunshine.svg?v=0.11.2)](https://opensource.org/licenses/ISC)
[![NPM Link](https://img.shields.io/npm/v/bunshine?v=0.12.0)](https://npmjs.com/package/bunshine)
[![Dependencies](https://badgen.net/static/dependencies/3/green?v=0.12.0)](https://www.npmjs.com/package/bunshine?activeTab=dependencies)
![Test Coverage: 97%](https://badgen.net/static/test%20coverage/96%25/green?v=0.12.0)
[![ISC License](https://img.shields.io/npm/l/bunshine.svg?v=0.12.0)](https://opensource.org/licenses/ISC)

@@ -12,3 +12,5 @@ # Bunshine

`bun install bunshine`
```shell
bun add bunshine
```

@@ -25,5 +27,6 @@ ## Motivation

8. Provide common middleware out of the box
9. Make specifically for Bun
10. Comprehensive unit tests
11. Support for `X-HTTP-Method-Override` header
9. Built-in gzip compression
10. Make specifically for Bun
11. Comprehensive unit tests
12. Support for `X-HTTP-Method-Override` header

@@ -65,16 +68,16 @@ ## Table of Contents

```ts
import { HttpRouter, json, redirect } from 'bunshine';
import { HttpRouter, redirect } from 'bunshine';
const app = new HttpRouter();
app.patch('/users/:id', async ({ request, params, url }) => {
await authorize(request.headers.get('Authorization'));
const data = await request.json();
app.patch('/users/:id', async c => {
await authorize(c.request.headers.get('Authorization'));
const data = await c.request.json();
const result = await updateUser(params.id, data);
if (result === 'not found') {
return json({ error: 'User not found' }, { status: 404 });
return c.json({ error: 'User not found' }, { status: 404 });
} else if (result === 'error') {
return json({ error: 'Error updating user' }, { status: 500 });
return c.json({ error: 'Error updating user' }, { status: 500 });
} else {
return json({ error: false });
return c.json({ error: false });
}

@@ -115,4 +118,6 @@ });

app.get('/hello', (c: Context, next: NextFunction) => {
// Properties of the Context object
c.request; // The raw request object
c.params; // The request params from URL placeholders
c.url; // The URL object
c.params; // The request params from route placeholders
c.server; // The Bun server instance (useful for pub-sub)

@@ -122,2 +127,16 @@ c.app; // The HttpRouter instance

c.error; // Handlers registered with app.on500() can see this Error object
c.ip; // The IP address of the client (not necessarily the end user)
c.date; // The date of the request
c.now; // The result of performance.now() at the start of the request
// Convenience methods for creating Response objects with various content types
// Note that responses are automatically gzipped if the client accepts gzip
c.json(data, init);
c.text(text, init);
c.js(jsText, init);
c.xml(xmlText, init);
c.html(htmlText, init);
c.css(cssText, init);
c.file(path, init);
// Create a redirect Response
c.redirect(url, status);
});

@@ -159,2 +178,3 @@ ```

}
// continue to other handlers
});

@@ -164,6 +184,9 @@

app.use(async (c, next) => {
// wait for response from other handlers
const resp = await next();
// peek at status and log if 403
if (resp.status === 403) {
logThatUserWasForbidden(c.request.url);
}
// return the response from the other handlers
return resp;

@@ -254,2 +277,3 @@ });

app.get('/hello', (c: Context, next: NextFunction) => {
// wait for other handlers to return a response
const resp = next();

@@ -261,2 +285,3 @@ // do stuff with response

app.get('/hello', async (c: Context, next: NextFunction) => {
// wait for other handlers to return a response
const resp = await next();

@@ -292,2 +317,3 @@ // do stuff with response

// Bunshine accepts any number of middleware functions in parameters or arrays
// so the following are equivalent
app.get('/posts', middleware1, middleware2, handler);

@@ -389,3 +415,3 @@ app.get('/users', [middleware1, middleware2, handler]);

gameRoom.onerror = handleGameError;
// sending messages
// send message to server
gameRoom.send(JSON.stringify({ type: 'GameMove', move: 'rock' }));

@@ -541,2 +567,4 @@ ```

### Path examples
| Path | URL | params |

@@ -558,2 +586,27 @@ | ---------------------- | --------------------- | ------------------------ |

### HTTP methods
```ts
import { HttpRouter } from 'bunshine';
const app = new HttpRouter();
app.head('/posts/:id', doesPostExist);
app.get('/posts/:id', getPost);
app.post('/posts/:id', addPost);
app.patch('/posts/:id', editPost);
app.put('/posts/:id', upsertPost);
app.trace('/posts/:id', tracePost);
app.delete('/posts/:id', deletePost);
app.options('/posts/:id', getPostCors);
// special case for specifying both head and get
app.headGet('/files/*', serveFiles(`${import.meta.dir}/files`));
// any list of multiple verbs (must be uppercase)
app.on(['POST', 'PATCH'], '/posts/:id', addEditPost);
app.listen({ port: 3100 });
```
## Included middleware

@@ -565,3 +618,3 @@

easy with the `serveFiles` middleware. Note that ranged requests are
supported, so you can use this for video streaming or partial downloads.
supported, so you can use it for video streaming or partial downloads.

@@ -920,10 +973,19 @@ ```ts

- ✅ middleware > trailingSlashes
- 🔲 middleware > compression
- 🔲 options for serveFiles
- 🔲 middleware > directoryListing
- 🔲 middleware > rate limiter
- ✅ gzip compression
- ✅ options for serveFiles
- 🔲 tests for cors
- 🔲 tests for devLogger
- 🔲 tests for prodLogger
- 🔲 tests for serveFiles
- 🔲 more examples
- ✅ tests for serveFiles
- 🔲 add flags to bin/serve.ts with commander
- 🔲 document flags for `bunx bunshine serve`
- 🔲 more files in examples folder
- 🔲 example of mini app that uses bin/serve.ts (maybe our own docs?)
- 🔲 GitHub Actions to run tests and coverage
- 🔲 Fix TypeScript warnings
- 🔲 Support server clusters
- ✅ Export functions to gzip strings and files
- ✅ Gzip performance testing (to get min/max defaults)

@@ -930,0 +992,0 @@ ## License

import type { Server } from 'bun';
import { describe, expect, it } from 'bun:test';
import { beforeEach, describe, expect, it } from 'bun:test';
import HttpRouter from '../HttpRouter/HttpRouter';
import {
file,
html,
js,
json,
redirect,
text,
xml,
} from '../HttpRouter/responseFactories.ts';
import Context from './Context';

@@ -90,54 +81,67 @@

});
it('should return 404 on file not found', async () => {
const resp = await file(`${import.meta.dir}/invalidfile`);
expect(resp.status).toBe(404);
describe('server', () => {
let c: Context;
beforeEach(() => {
const request = new Request('http://localhost/thing');
const app = new HttpRouter();
c = new Context(request, server, app);
});
it('should return 404 on file not found', async () => {
const resp = await c.file(`${import.meta.dir}/invalidfile`);
expect(resp.status).toBe(404);
});
it('should include text()', async () => {
const resp = c.text('Hi');
expect(await resp.text()).toBe('Hi');
expect(resp.headers.get('Content-type')).toStartWith('text/plain');
});
it('should include js()', async () => {
const resp = c.js('alert(42)');
expect(await resp.text()).toBe('alert(42)');
expect(resp.headers.get('Content-type')).toStartWith('text/javascript');
});
it('should include html()', async () => {
const resp = c.html('<h1>Hi</h1>');
expect(await resp.text()).toBe('<h1>Hi</h1>');
expect(resp.headers.get('Content-type')).toStartWith('text/html');
});
it('should include css()', async () => {
const resp = c.css('* { min-width: 0 }');
expect(await resp.text()).toBe('* { min-width: 0 }');
expect(resp.headers.get('Content-type')).toStartWith('text/css');
});
it('should include xml()', async () => {
const resp = c.xml('<greeting>Hi</greeting>');
expect(await resp.text()).toBe('<greeting>Hi</greeting>');
expect(resp.headers.get('Content-type')).toStartWith('text/xml');
});
it('should include json(data)', async () => {
const resp = c.json({ hello: 'world' });
expect(await resp.json()).toEqual({ hello: 'world' });
expect(resp.headers.get('Content-type')).toStartWith('application/json');
});
it('should include json(data, init)', async () => {
const resp = c.json(
{ hello: 'world' },
{
headers: {
'X-Hello': 'World',
},
}
);
expect(await resp.json()).toEqual({ hello: 'world' });
expect(resp.headers.get('Content-type')).toStartWith('application/json');
expect(resp.headers.get('X-Hello')).toBe('World');
});
// it('should include redirect(url)', () => {
// const resp = redirect('/home');
// expect(resp.headers.get('Location')).toBe('/home');
// expect(resp.status).toBe(302);
// });
// it('should include redirect(url, status)', () => {
// const resp = redirect('/home', 301);
// expect(resp.headers.get('Location')).toBe('/home');
// expect(resp.status).toBe(301);
// });
});
it('should include text()', async () => {
const resp = text('Hi');
expect(await resp.text()).toBe('Hi');
expect(resp.headers.get('Content-type')).toStartWith('text/plain');
});
it('should include js()', async () => {
const resp = js('alert(42)');
expect(await resp.text()).toBe('alert(42)');
expect(resp.headers.get('Content-type')).toStartWith('text/javascript');
});
it('should include html()', async () => {
const resp = html('<h1>Hi</h1>');
expect(await resp.text()).toBe('<h1>Hi</h1>');
expect(resp.headers.get('Content-type')).toStartWith('text/html');
});
it('should include xml()', async () => {
const resp = xml('<greeting>Hi</greeting>');
expect(await resp.text()).toBe('<greeting>Hi</greeting>');
expect(resp.headers.get('Content-type')).toStartWith('text/xml');
});
it('should include json(data)', async () => {
const resp = json({ hello: 'world' });
expect(await resp.json()).toEqual({ hello: 'world' });
expect(resp.headers.get('Content-type')).toStartWith('application/json');
});
it('should include json(data, init)', async () => {
const resp = json(
{ hello: 'world' },
{
headers: {
'X-Hello': 'World',
},
}
);
expect(await resp.json()).toEqual({ hello: 'world' });
expect(resp.headers.get('Content-type')).toStartWith('application/json');
expect(resp.headers.get('X-Hello')).toBe('World');
});
it('should include redirect(url)', () => {
const resp = redirect('/home');
expect(resp.headers.get('Location')).toBe('/home');
expect(resp.status).toBe(302);
});
it('should include redirect(url, status)', () => {
const resp = redirect('/home', 301);
expect(resp.headers.get('Location')).toBe('/home');
expect(resp.status).toBe(301);
});
});
import type { BunFile, Server } from 'bun';
import type HttpRouter from '../HttpRouter/HttpRouter';
import {
factory,
file,
html,
js,
json,
redirect,
sse,
text,
xml,
type FileResponseOptions,

@@ -16,2 +13,8 @@ type SseSetupFunction,

const textPlain = factory('text/plain');
const textJs = factory('text/javascript');
const textHtml = factory('text/html');
const textXml = factory('text/xml');
const textCss = factory('text/css');
export default class Context<

@@ -22,2 +25,4 @@ ParamsShape extends Record<string, string> = Record<string, string>,

request: Request;
/** Alias for `request` */
req: Request;
/** The Bun server instance */

@@ -42,2 +47,3 @@ server: Server;

this.request = request;
this.req = request;
this.server = server;

@@ -54,18 +60,34 @@ this.app = app;

/** A shorthand for `new Response(text, { headers: { 'Content-type': 'text/plain' } })` */
text = text;
text(text: string, init: ResponseInit = {}) {
return textPlain.call(this, text, init);
}
/** A shorthand for `new Response(js, { headers: { 'Content-type': 'text/javascript' } })` */
js = js;
js(js: string, init: ResponseInit = {}) {
return textJs.call(this, js, init);
}
/** A shorthand for `new Response(html, { headers: { 'Content-type': 'text/html' } })` */
html = html;
html(html: string, init: ResponseInit = {}) {
return textHtml.call(this, html, init);
}
/** A shorthand for `new Response(html, { headers: { 'Content-type': 'text/css' } })` */
css(css: string, init: ResponseInit = {}) {
return textCss.call(this, css, init);
}
/** A shorthand for `new Response(xml, { headers: { 'Content-type': 'text/xml' } })` */
xml = xml;
xml(xml: string, init: ResponseInit = {}) {
return textXml.call(this, xml, init);
}
/** A shorthand for `new Response(JSON.stringify(data), { headers: { 'Content-type': 'application/json' } })` */
json = json;
json(data: any, init: ResponseInit = {}) {
return json.call(this, data, init);
}
/** A shorthand for `new Response(null, { headers: { Location: url }, status: 301 })` */
redirect = redirect;
redirect(url: string, status = 302) {
return redirect(url, status);
}
/** A shorthand for `new Response(fileBody, fileHeaders)` */
file = async (
async file(
filenameOrBunFile: string | BunFile,
fileOptions: FileResponseOptions = {}
) => {
) {
return file(filenameOrBunFile, {

@@ -75,7 +97,7 @@ range: this.request.headers.get('Range') || undefined,

});
};
}
/** A shorthand for `new Response({ headers: { 'Content-type': 'text/event-stream' } })` */
sse = (setup: SseSetupFunction, init: ResponseInit = {}) => {
sse(setup: SseSetupFunction, init: ResponseInit = {}) {
return sse(this.request.signal, setup, init);
};
}
}

@@ -126,4 +126,4 @@ import type { Server } from 'bun';

it('should extract params', async () => {
app.get('/users/:id', ({ params }) => {
throw new Response(params.id, {
app.get('/users/:id', c => {
throw new Response(c.params.id, {
status: 200,

@@ -143,4 +143,4 @@ headers: {

it('should give params for * routes', async () => {
app.get('/abc/*', ({ params }) => {
throw new Response(params[0], {
app.get('/abc/*', c => {
throw new Response(c.params[0], {
status: 200,

@@ -160,4 +160,4 @@ headers: {

it('should allow registering multiple methods', async () => {
app.on(['POST', 'PUT'], '/user', ({ request, text }) => {
return text('Method was ' + request.method);
app.on(['POST', 'PUT'], '/user', c => {
return new Response('Method was ' + c.request.method);
});

@@ -178,7 +178,14 @@ const resp = await app.fetch(

it('should allow RegExp paths', async () => {
app.get(/^\/user\/(.+)\/(.+)/, ({ params, url, json }) => {
return json({
pathname: url.pathname,
params,
});
app.get(/^\/user\/(.+)\/(.+)/, c => {
return new Response(
JSON.stringify({
pathname: c.url.pathname,
params: c.params,
}),
{
headers: {
'Content-type': 'application/json',
},
}
);
});

@@ -296,2 +303,11 @@ const resp = await app.fetch(

});
it('should emit url', async () => {
app.all('/', () => new Response('Hi'));
server = app.listen({ port: 7772 });
let output: string;
const to = (message: string) => (output = message);
app.emitUrl({ to });
// @ts-expect-error
expect(output).toContain(String(server.url));
});
it('should handle all', async () => {

@@ -312,6 +328,4 @@ app.all('/', () => new Response('Hi'));

it('should handle PUT', async () => {
let body: { name: string } = { name: '' };
app.put('/', async ({ request }) => {
body = await request.json();
return new Response('Hi');
app.put('/', async ({ request, json }) => {
return json(await request.json());
});

@@ -326,2 +340,3 @@ server = app.listen({ port: 7774 });

});
const body = await resp.json();
expect(resp.status).toBe(200);

@@ -370,3 +385,2 @@ expect(body).toEqual({ name: 'Alice' });

expect(resp.status).toBe(200);
// @ts-expect-error
expect(await resp.json()).toEqual({ key: 'secret' });

@@ -393,10 +407,7 @@ });

expect(resp.status).toBe(200);
// @ts-expect-error
expect(await resp.json()).toEqual({ key2: 'secret2' });
});
it('should handle PATCH', async () => {
let body: { name: string } = { name: '' };
app.patch('/', async ({ request }) => {
body = await request.json();
return new Response('Hi');
app.patch('/', async ({ request, json }) => {
return json(await request.json());
});

@@ -411,2 +422,3 @@ server = app.listen({ port: 7778 });

});
const body = await resp.json();
expect(resp.status).toBe(200);

@@ -413,0 +425,0 @@ expect(body).toEqual({ name: 'Charlie' });

import type { ServeOptions, Server } from 'bun';
// @ts-ignore
import os from 'os';
import bunshine from '../../package.json';

@@ -66,2 +67,3 @@ import Context from '../Context/Context';

verbose?: boolean;
to?: (message: string) => void;
};

@@ -83,3 +85,3 @@

}
respectSigTerm = ({ closeActiveConnections = true } = {}) => {
respectSigTerm({ closeActiveConnections = true } = {}) {
['SIGTERM', 'SIGINT'].forEach(signal => {

@@ -91,9 +93,10 @@ process.once(signal, () => {

});
};
listen = (options: Omit<ServeOptions, 'fetch'> = {}) => {
return this;
}
listen(options: Omit<ServeOptions, 'fetch'> = {}) {
const server = Bun.serve(this.getExport(options));
this.server = server;
return server;
};
emitUrl = (options: EmitUrlOptions = { verbose: false }) => {
}
emitUrl({ verbose = false, to = console.log }: EmitUrlOptions = {}) {
if (!this.server) {

@@ -105,14 +108,17 @@ throw new Error(

const servingAt = String(this.server.url);
if (options.verbose) {
const server = Bun.env.COMPUTERNAME || Bun.env.HOSTNAME;
if (verbose) {
const server = Bun.env.COMPUTERNAME || os.hostname();
const mode = Bun.env.NODE_ENV || 'production';
const took = Math.round(performance.now());
console.log(
`☀️ Bunshine v${bunshine.version} on Bun v${Bun.version} running at ${servingAt} on server "${server}" in ${mode} (${took}ms)`
const runtime = process.versions.bun
? `Bun v${process.versions.bun}`
: `Node v${process.versions.node}`;
to(
`☀️ Bunshine v${bunshine.version} on ${runtime} serving at ${servingAt} on "${server}" in ${mode} (${took}ms)`
);
} else {
console.log(`☀️ Serving ${servingAt}`);
to(`☀️ Serving ${servingAt}`);
}
};
getExport = (options: Omit<ServeOptions, 'fetch' | 'websocket'> = {}) => {
}
getExport(options: Omit<ServeOptions, 'fetch' | 'websocket'> = {}) {
const config = {

@@ -128,3 +134,3 @@ port: 0,

return config;
};
}
get socket() {

@@ -136,7 +142,7 @@ if (!this._wsRouter) {

}
on = <ParamsShape extends Record<string, string> = Record<string, string>>(
on<ParamsShape extends Record<string, string> = Record<string, string>>(
verbOrVerbs: HttpMethods | HttpMethods[],
path: string | RegExp,
...handlers: Handler<ParamsShape>[]
) => {
) {
if (Array.isArray(verbOrVerbs)) {

@@ -155,55 +161,74 @@ for (const verb of verbOrVerbs) {

return this;
};
all = <ParamsShape extends Record<string, string> = Record<string, string>>(
}
all<ParamsShape extends Record<string, string> = Record<string, string>>(
path: string | RegExp,
...handlers: Handler<ParamsShape>[]
) => this.on<ParamsShape>('ALL', path, handlers);
get = <ParamsShape extends Record<string, string> = Record<string, string>>(
) {
return this.on<ParamsShape>('ALL', path, handlers);
}
get<ParamsShape extends Record<string, string> = Record<string, string>>(
path: string | RegExp,
...handlers: Handler<ParamsShape>[]
) => this.on<ParamsShape>('GET', path, handlers);
put = <ParamsShape extends Record<string, string> = Record<string, string>>(
) {
return this.on<ParamsShape>('GET', path, handlers);
}
put<ParamsShape extends Record<string, string> = Record<string, string>>(
path: string | RegExp,
...handlers: Handler<ParamsShape>[]
) => this.on<ParamsShape>('PUT', path, handlers);
head = <ParamsShape extends Record<string, string> = Record<string, string>>(
) {
return this.on<ParamsShape>('PUT', path, handlers);
}
head<ParamsShape extends Record<string, string> = Record<string, string>>(
path: string | RegExp,
...handlers: Handler<ParamsShape>[]
) => this.on<ParamsShape>('HEAD', path, handlers);
post = <ParamsShape extends Record<string, string> = Record<string, string>>(
) {
return this.on<ParamsShape>('HEAD', path, handlers);
}
post<ParamsShape extends Record<string, string> = Record<string, string>>(
path: string | RegExp,
...handlers: Handler<ParamsShape>[]
) => this.on<ParamsShape>('POST', path, handlers);
patch = <ParamsShape extends Record<string, string> = Record<string, string>>(
) {
return this.on<ParamsShape>('POST', path, handlers);
}
patch<ParamsShape extends Record<string, string> = Record<string, string>>(
path: string | RegExp,
...handlers: Handler<ParamsShape>[]
) => this.on<ParamsShape>('PATCH', path, handlers);
trace = <ParamsShape extends Record<string, string> = Record<string, string>>(
) {
return this.on<ParamsShape>('PATCH', path, handlers);
}
trace<ParamsShape extends Record<string, string> = Record<string, string>>(
path: string | RegExp,
...handlers: Handler<ParamsShape>[]
) => this.on<ParamsShape>('TRACE', path, handlers);
delete = <
ParamsShape extends Record<string, string> = Record<string, string>,
>(
) {
return this.on<ParamsShape>('TRACE', path, handlers);
}
delete<ParamsShape extends Record<string, string> = Record<string, string>>(
path: string | RegExp,
...handlers: Handler<ParamsShape>[]
) => this.on<ParamsShape>('DELETE', path, handlers);
options = <
ParamsShape extends Record<string, string> = Record<string, string>,
>(
) {
return this.on<ParamsShape>('DELETE', path, handlers);
}
options<ParamsShape extends Record<string, string> = Record<string, string>>(
path: string | RegExp,
...handlers: Handler<ParamsShape>[]
) => this.on<ParamsShape>('OPTIONS', path, handlers);
use = (...handlers: Handler<{}>[]) => {
this.all('*', handlers);
return this;
};
onError = (...handlers: Handler<Record<string, string>>[]) => {
) {
return this.on<ParamsShape>('OPTIONS', path, handlers);
}
headGet<ParamsShape extends Record<string, string> = Record<string, string>>(
path: string | RegExp,
...handlers: Handler<ParamsShape>[]
) {
return this.on<ParamsShape>(['HEAD', 'GET'], path, handlers);
}
use(...handlers: Handler<{}>[]) {
return this.all('*', handlers);
}
onError(...handlers: Handler<Record<string, string>>[]) {
this._onErrors.push(...handlers.flat(9));
return this;
};
on404 = (...handlers: Handler<Record<string, string>>[]) => {
}
on404(...handlers: Handler<Record<string, string>>[]) {
this._on404s.push(...handlers.flat(9));
return this;
};
}
fetch = async (request: Request, server: Server) => {

@@ -215,3 +240,2 @@ const context = new Context(request, server, this);

).toUpperCase();
// @ts-expect-error
const filter = filters[method] || getPathMatchFilter(method);

@@ -218,0 +242,0 @@ const matched = this.pathMatcher.match(pathname, filter, this._on404s);

import { BunFile } from 'bun';
import path from 'node:path';
import Context from '../Context/Context.ts';
import { gzipString } from '../gzip/gzip.ts';
export const text = getResponseFactory('text/plain; charset=utf-8');
export const js = getResponseFactory('text/javascript; charset=utf-8');
export const html = getResponseFactory('text/html; charset=utf-8');
export const xml = getResponseFactory('text/xml; charset=utf-8');
export const json = (data: any, init: ResponseInit = {}) => {
return new Response(JSON.stringify(data), {
...init,
export type Factory = (body: string, init?: ResponseInit) => Response;
const textEncoder = new TextEncoder();
// body must be large enough to be worth compressing
// (54 is minimum size of gzip after metadata; 100 is arbitrary choice)
export let minGzipSize = 100;
export function json(this: Context, data: any, init: ResponseInit = {}) {
let body: string | Uint8Array = JSON.stringify(data);
// @ts-expect-error
init.headers = new Headers(init.headers || {});
init.headers.set('Content-type', `application/json; charset=utf-8`);
// body must be large enough to be worth compressing
if (body.length >= minGzipSize) {
body = gzipString(body);
init.headers.set('Content-Encoding', 'gzip');
init.headers.set('Content-Length', String(body.length));
}
// @ts-expect-error
return new Response(body, init);
}
export function factory(contentType: string): Factory {
return function (this: Context, body: string, init: ResponseInit = {}) {
// @ts-expect-error
headers: {
...(init.headers || {}),
'Content-Type': 'application/json; charset=utf-8',
},
});
};
init.headers = new Headers(init.headers || {});
init.headers.set('Content-type', `${contentType}; charset=utf-8`);
if (
// client must expect gzip
this.request.headers.get('Accept-Encoding')?.includes('gzip') &&
// body must be large enough to be worth compressing
body.length >= minGzipSize
) {
// @ts-expect-error
body = gzipString(body);
init.headers.set('Content-Encoding', 'gzip');
}
init.headers.set('Content-Length', String(body.length));
// @ts-expect-error
return new Response(body, init);
};
}

@@ -30,2 +62,5 @@ export const redirect = (url: string, status = 302) => {

chunkSize?: number;
gzip?: boolean;
disposition?: 'inline' | 'attachment';
acceptRanges?: boolean;
};

@@ -40,4 +75,3 @@ export const file = async (

: filenameOrBunFile;
const totalFileSize = file.size;
if (totalFileSize === 0) {
if (!(await file.exists())) {
return new Response('File not found', { status: 404 });

@@ -51,5 +85,17 @@ }

method: 'GET',
gzip: fileOptions.gzip,
});
// tell the client that we are capable of handling range requests
resp.headers.set('Accept-Ranges', 'bytes');
if (fileOptions.acceptRanges !== false) {
// tell the client that we are capable of handling range requests
resp.headers.set('Accept-Ranges', 'bytes');
}
if (fileOptions.disposition === 'attachment') {
const filename = path.basename(file.name!);
resp.headers.set(
'Content-Disposition',
`${fileOptions.disposition}; filename="${filename}"`
);
} else if (fileOptions.disposition === 'inline') {
resp.headers.set('Content-Disposition', 'inline');
}
return resp;

@@ -77,6 +123,2 @@ };

async start(controller: ReadableStreamDefaultController) {
// Step 1: create encoder to handle utf8
const encoder = new TextEncoder();
// Step 2: define the send and close functions
function send(

@@ -90,3 +132,3 @@ eventName: string,

if (arguments.length === 1) {
encoded = encoder.encode(`data: ${eventName}\n\n`);
encoded = textEncoder.encode(`data: ${eventName}\n\n`);
} else {

@@ -104,3 +146,3 @@ if (data && typeof data !== 'string') {

message += '\n\n';
encoded = encoder.encode(message);
encoded = textEncoder.encode(message);
}

@@ -161,15 +203,2 @@ if (signal.aborted) {

function getResponseFactory(contentType: string) {
return function (content: any, init: ResponseInit = {}) {
return new Response(content, {
...init,
// @ts-ignore
headers: {
...(init.headers || {}),
'Content-Type': contentType,
},
});
};
}
export async function buildFileResponse({

@@ -181,3 +210,3 @@ file,

method,
responseInit,
gzip,
}: {

@@ -189,3 +218,3 @@ file: BunFile;

method: string;
responseInit?: ResponseInit;
gzip?: boolean;
}) {

@@ -192,0 +221,0 @@ let response: Response;

import type { Server } from 'bun';
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { globby } from 'globby';
import fs from 'node:fs/promises';
import path from 'path';

@@ -299,2 +301,333 @@ import HttpRouter from '../../HttpRouter/HttpRouter.ts';

});
describe('gzip', () => {
describe('NeverCache', () => {
it('should gzip file on demand', async () => {
app.get(
'/toGzip/*',
serveFiles(`${fixturesPath}/toGzip`, {
gzip: {
minFileSize: 0,
maxFileSize: 100000,
cache: { type: 'never' },
},
}),
c => c.text('Hello')
);
server = app.listen();
const resp = await fetch(`${server.url}toGzip/1.js`);
const text = await resp.text();
expect(text).toBe('// This is file number one\n');
expect(resp.headers.get('content-encoding')).toBe('gzip');
});
it('should ignore if file is too small', async () => {
app.get(
'/toGzip/*',
serveFiles(`${fixturesPath}/toGzip`, {
gzip: {
minFileSize: 1000,
maxFileSize: 100000,
cache: { type: 'never' },
},
}),
c => c.text('Hello')
);
server = app.listen();
const resp = await fetch(`${server.url}toGzip/1.js`);
const text = await resp.text();
expect(text).toBe('// This is file number one\n');
expect(resp.headers.get('content-encoding')).toBe(null);
});
it('should ignore if file is too big', async () => {
app.get(
'/toGzip/*',
serveFiles(`${fixturesPath}/toGzip`, {
gzip: {
minFileSize: 0,
maxFileSize: 10,
cache: { type: 'never' },
},
}),
c => c.text('Hello')
);
server = app.listen();
const resp = await fetch(`${server.url}toGzip/1.js`);
const text = await resp.text();
expect(text).toBe('// This is file number one\n');
expect(resp.headers.get('content-encoding')).toBe(null);
});
it('should not gzip jpeg file', async () => {
app.get(
'/toGzip/*',
serveFiles(`${fixturesPath}/toGzip`, {
gzip: {
minFileSize: 0,
maxFileSize: 1e6,
cache: { type: 'never' },
},
}),
c => c.text('Hello')
);
server = app.listen();
const resp = await fetch(`${server.url}toGzip/dream.jpg`);
expect(resp.headers.get('content-encoding')).toBe(null);
});
});
describe('MemoryCache', () => {
it('should gzip', async () => {
app.get(
'/toGzip/*',
serveFiles(`${fixturesPath}/toGzip`, {
gzip: {
minFileSize: 0,
maxFileSize: 100000,
cache: { type: 'memory', maxBytes: 500 },
},
}),
c => c.text('Hello')
);
server = app.listen();
const resp = await fetch(`${server.url}toGzip/2.css`);
const text = await resp.text();
expect(text).toBe('/* This is file number two */\n');
expect(resp.headers.get('content-encoding')).toBe('gzip');
});
it('should ignore if file is too small', async () => {
app.get(
'/toGzip/*',
serveFiles(`${fixturesPath}/toGzip`, {
gzip: {
minFileSize: 1000,
maxFileSize: 100000,
cache: { type: 'memory', maxBytes: 500 },
},
}),
c => c.text('Hello')
);
server = app.listen();
const resp = await fetch(`${server.url}toGzip/1.js`);
const text = await resp.text();
expect(text).toBe('// This is file number one\n');
expect(resp.headers.get('content-encoding')).toBe(null);
});
it('should ignore if file is too big', async () => {
app.get(
'/toGzip/*',
serveFiles(`${fixturesPath}/toGzip`, {
gzip: {
minFileSize: 0,
maxFileSize: 10,
cache: { type: 'memory', maxBytes: 500 },
},
}),
c => c.text('Hello')
);
server = app.listen();
const resp = await fetch(`${server.url}toGzip/1.js`);
const text = await resp.text();
expect(text).toBe('// This is file number one\n');
expect(resp.headers.get('content-encoding')).toBe(null);
});
it('should not gzip jpeg file', async () => {
app.get(
'/toGzip/*',
serveFiles(`${fixturesPath}/toGzip`, {
gzip: {
minFileSize: 0,
maxFileSize: 1e6,
cache: { type: 'never' },
},
}),
c => c.text('Hello')
);
server = app.listen();
const resp = await fetch(`${server.url}toGzip/dream.jpg`);
expect(resp.headers.get('content-encoding')).toBe(null);
});
});
describe('FileCache', () => {
beforeEach(async () => {
const paths = await globby(['/tmp/*~src~testFixtures~toGzip~*.gz']);
for (const path of paths) {
await fs.unlink(path);
}
});
it('should gzip with file cache', async () => {
app.get(
'/toGzip/*',
serveFiles(`${fixturesPath}/toGzip`, {
gzip: {
minFileSize: 0,
maxFileSize: 100000,
cache: { type: 'file', maxBytes: 500, path: '/tmp' },
},
}),
c => c.text('Hello')
);
server = app.listen();
const resp = await fetch(`${server.url}toGzip/1.js`);
const text = await resp.text();
expect(text).toBe('// This is file number one\n');
expect(resp.headers.get('content-encoding')).toBe('gzip');
// check if it properly disposes of the oldest accessed file (1.js)
await fetch(`${server.url}toGzip/2.css`);
await fetch(`${server.url}toGzip/3.html`);
const paths = await globby(['/tmp/*~src~testFixtures~toGzip~*.gz']);
expect(paths.some(p => p.includes('1.js'))).toBe(false);
expect(paths.some(p => p.includes('2.css'))).toBe(true);
expect(paths.some(p => p.includes('3.html'))).toBe(true);
});
it('should ignore if file is too small', async () => {
app.get(
'/toGzip/*',
serveFiles(`${fixturesPath}/toGzip`, {
gzip: {
minFileSize: 1000,
maxFileSize: 100000,
cache: { type: 'file', maxBytes: 500, path: '/tmp' },
},
}),
c => c.text('Hello')
);
server = app.listen();
const resp = await fetch(`${server.url}toGzip/1.js`);
const text = await resp.text();
expect(text).toBe('// This is file number one\n');
expect(resp.headers.get('content-encoding')).toBe(null);
});
it('should ignore if file is too big', async () => {
app.get(
'/toGzip/*',
serveFiles(`${fixturesPath}/toGzip`, {
gzip: {
minFileSize: 0,
maxFileSize: 10,
cache: { type: 'file', maxBytes: 500, path: '/tmp' },
},
}),
c => c.text('Hello')
);
server = app.listen();
const resp = await fetch(`${server.url}toGzip/1.js`);
const text = await resp.text();
expect(text).toBe('// This is file number one\n');
expect(resp.headers.get('content-encoding')).toBe(null);
});
it('should not gzip jpeg file', async () => {
app.get(
'/toGzip/*',
serveFiles(`${fixturesPath}/toGzip`, {
gzip: {
minFileSize: 0,
maxFileSize: 1e6,
cache: { type: 'never' },
},
}),
c => c.text('Hello')
);
server = app.listen();
const resp = await fetch(`${server.url}toGzip/dream.jpg`);
expect(resp.headers.get('content-encoding')).toBe(null);
});
});
describe('PrecompressCache', () => {
beforeEach(async () => {
const paths = await globby(['/tmp/*~src~testFixtures~toGzip~**.gz']);
for (const path of paths) {
await fs.unlink(path);
}
});
it('should gzip with file cache', async () => {
app.get(
'/toGzip/*',
serveFiles(`${fixturesPath}/toGzip`, {
gzip: {
minFileSize: 0,
maxFileSize: 100000,
cache: { type: 'precompress', maxBytes: 7500, path: '/tmp' },
},
}),
c => c.text('Hello')
);
server = app.listen();
const resp = await fetch(`${server.url}toGzip/1.js`);
const text = await resp.text();
expect(text).toBe('// This is file number one\n');
expect(resp.headers.get('content-encoding')).toBe('gzip');
// check if it properly pre-zipped all 4 files
const paths = await globby(['/tmp/*~src~testFixtures~toGzip~**.gz']);
expect(paths).toHaveLength(4);
});
it('should gzip some with file cache under limited maxBytes', async () => {
app.get(
'/toGzip/*',
serveFiles(`${fixturesPath}/toGzip`, {
gzip: {
minFileSize: 0,
maxFileSize: 100000,
cache: { type: 'precompress', maxBytes: 75, path: '/tmp' },
},
}),
c => c.text('Hello')
);
server = app.listen();
const resp = await fetch(`${server.url}toGzip/1.js`);
const text = await resp.text();
// check if it properly pre-zipped the files
const paths = await globby(['/tmp/*~src~testFixtures~toGzip~*.gz']);
expect(paths).toHaveLength(2);
});
it('should ignore if file is too small', async () => {
app.get(
'/toGzip/*',
serveFiles(`${fixturesPath}/toGzip`, {
gzip: {
minFileSize: 1000,
maxFileSize: 100000,
cache: { type: 'precompress', maxBytes: 500, path: '/tmp' },
},
}),
c => c.text('Hello')
);
server = app.listen();
const resp = await fetch(`${server.url}toGzip/1.js`);
const text = await resp.text();
expect(text).toBe('// This is file number one\n');
expect(resp.headers.get('content-encoding')).toBe(null);
});
it('should ignore if file is too big', async () => {
app.get(
'/toGzip/*',
serveFiles(`${fixturesPath}/toGzip`, {
gzip: {
minFileSize: 0,
maxFileSize: 10,
cache: { type: 'precompress', maxBytes: 500, path: '/tmp' },
},
}),
c => c.text('Hello')
);
server = app.listen();
const resp = await fetch(`${server.url}toGzip/1.js`);
const text = await resp.text();
expect(text).toBe('// This is file number one\n');
expect(resp.headers.get('content-encoding')).toBe(null);
});
it('should not gzip jpeg file', async () => {
app.get(
'/toGzip/*',
serveFiles(`${fixturesPath}/toGzip`, {
gzip: {
minFileSize: 0,
maxFileSize: 1e6,
cache: { type: 'never' },
},
}),
c => c.text('Hello')
);
server = app.listen();
const resp = await fetch(`${server.url}toGzip/dream.jpg`);
expect(resp.headers.get('content-encoding')).toBe(null);
});
});
});
});

@@ -0,1 +1,2 @@

import { ZlibCompressionOptions } from 'bun';
import ms from 'ms';

@@ -5,2 +6,3 @@ import path from 'path';

import { buildFileResponse } from '../../HttpRouter/responseFactories.ts';
import { FileGzipper } from '../../gzip/FileGzipper.ts';

@@ -19,4 +21,34 @@ // see https://expressjs.com/en/4x/api.html#express.static

maxAge?: number | string;
gzip?: GzipOptions;
};
export type GzipOptions = {
minFileSize?: number;
maxFileSize?: number;
mimeTypes?: Array<string | RegExp>;
zlibOptions?: ZlibCompressionOptions;
cache:
| false
| {
type: 'file' | 'precompress' | 'memory' | 'never';
maxBytes?: number;
path?: string;
};
};
const defaultGzipOptions: GzipOptions = {
minFileSize: 1024,
maxFileSize: 1024 * 1024 * 25,
mimeTypes: [
/^text\/.*/,
/^application\/json/,
/^image\/svg/,
/^font\/(otf|ttf|eot)/,
],
zlibOptions: {},
cache: {
type: 'never',
},
};
export function serveFiles(

@@ -34,2 +66,3 @@ directory: string,

maxAge = undefined,
gzip = undefined,
}: StaticOptions = {}

@@ -39,3 +72,10 @@ ): Middleware {

maxAge === undefined ? null : getCacheControl(maxAge, immutable);
const gzipper = gzip
? new FileGzipper(directory, { ...defaultGzipOptions, ...gzip })
: undefined;
return async c => {
if (gzipper) {
// wait for setup cache if not done already
await gzipper.setupPromise;
}
const filename = c.params[0] || c.url.pathname;

@@ -83,10 +123,17 @@ if (filename.startsWith('.')) {

}
// get base response
const response = await buildFileResponse({
file,
acceptRanges,
chunkSize: 0,
rangeHeader: c.request.headers.get('range'),
method: c.request.method,
});
const rangeHeader = c.request.headers.get('range');
let response: Response;
if (rangeHeader || !gzipper) {
// get base response
response = await buildFileResponse({
file,
acceptRanges,
chunkSize: 0,
rangeHeader,
method: c.request.method,
gzip: false,
});
} else {
response = await gzipper.fetch(file);
}
// add current date

@@ -93,0 +140,0 @@ response.headers.set('Date', new Date().toUTCString());

{
"compilerOptions": {
"include": ["*"],
"include": ["src/**/*"],
"lib": ["ESNext"],

@@ -9,2 +9,4 @@ "module": "esnext",

"moduleDetection": "force",
"isolatedModules": true,
"esModuleInterop": true,
"allowImportingTsExtensions": true,

@@ -16,3 +18,3 @@ "noEmit": true,

"skipLibCheck": true,
"jsx": "react-jsx",
"noImplicitAny": false,
"allowSyntheticDefaultImports": true,

@@ -19,0 +21,0 @@ "forceConsistentCasingInFileNames": true,

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc