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

bunshine

Package Overview
Dependencies
Maintainers
0
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 3.1.0 to 3.1.2

2

bin/serve.ts

@@ -21,3 +21,3 @@ #!/usr/bin/env bun

);
app.listen();
app.listen({ port: 0, reusePort: true });

@@ -24,0 +24,0 @@ console.log(`☀️ Bunshine serving static files at ${app.server!.url}`);

# Changelog
## v3.1.0
## v3.1.2 - Nov 25, 2024
- Change default range chunk size 3MB => 1MB
- Support passing headers to c.file()
- Return 206 in ranged downloads even if whole file is requested (Safari bug)
## v3.1.1 - Nov 23, 2024
- Fix Content-Range header when file size is 0
## v3.1.0 - Nov 23, 2024
- Remove useless exports of response factories

@@ -6,0 +16,0 @@ - More unit tests

{
"name": "bunshine",
"version": "3.1.0",
"version": "3.1.2",
"module": "server/server.ts",

@@ -5,0 +5,0 @@ "type": "module",

@@ -5,10 +5,10 @@ # Bunshine

<img alt="Bunshine Logo" src="https://github.com/kensnyder/bunshine/raw/main/packages/bunshine/assets/bunshine-logo.png?v=3.1.0" width="200" height="187" />
<img alt="Bunshine Logo" src="https://github.com/kensnyder/bunshine/raw/main/packages/bunshine/assets/bunshine-logo.png?v=3.1.2" width="200" height="187" />
[![NPM Link](https://img.shields.io/npm/v/bunshine?v=3.1.0)](https://npmjs.com/package/bunshine)
[![Language: TypeScript](https://badgen.net/static/language/TS?v=3.1.0)](https://github.com/search?q=repo:kensnyder/bunshine++language:TypeScript&type=code)
[![Code Coverage](https://codecov.io/gh/kensnyder/bunshine/graph/badge.svg?token=4LLWB8NBNT&v=3.1.0)](https://codecov.io/gh/kensnyder/bunshine)
[![Dependencies: 1](https://badgen.net/static/dependencies/1/green?v=3.1.0)](https://www.npmjs.com/package/bunshine?activeTab=dependencies)
![Tree shakeable](https://badgen.net/static/tree%20shakeable/yes/green?v=3.1.0)
[![ISC License](https://badgen.net/github/license/kensnyder/bunshine?v=3.1.0)](https://opensource.org/licenses/ISC)
[![NPM Link](https://img.shields.io/npm/v/bunshine?v=3.1.2)](https://npmjs.com/package/bunshine)
[![Language: TypeScript](https://badgen.net/static/language/TS?v=3.1.2)](https://github.com/search?q=repo:kensnyder/bunshine++language:TypeScript&type=code)
[![Code Coverage](https://codecov.io/gh/kensnyder/bunshine/graph/badge.svg?token=4LLWB8NBNT&v=3.1.2)](https://codecov.io/gh/kensnyder/bunshine)
[![Dependencies: 1](https://badgen.net/static/dependencies/1/green?v=3.1.2)](https://www.npmjs.com/package/bunshine?activeTab=dependencies)
![Tree shakeable](https://badgen.net/static/tree%20shakeable/yes/green?v=3.1.2)
[![ISC License](https://badgen.net/github/license/kensnyder/bunshine?v=3.1.2)](https://opensource.org/licenses/ISC)

@@ -65,3 +65,4 @@ ## Installation

14. [Roadmap](#roadmap)
15. [ISC License](./LICENSE.md)
15. [Change Log](./CHANGELOG.md)
16. [ISC License](./LICENSE.md)

@@ -188,16 +189,16 @@ ## Upgrading from 1.x to 2.x

// Or create Response objects with convenience functions:
return c.json(data, init);
return c.text(text, init);
return c.js(jsText, init);
return c.xml(xmlText, init);
return c.html(htmlText, init);
return c.css(cssText, init);
return c.file(pathOrSource, init);
return c.json(data, init); // data to pass to JSON.stringify
return c.text(text, init); // plain text
return c.js(jsText, init); // plain-text js
return c.xml(xmlText, init); // plain-text xml
return c.html(htmlText, init); // plain-text html
return c.css(cssText, init); // plain-text css
return c.file(pathOrSource, init); // file path, BunFile or binary source
// above init is ResponseInit:
{
headers: Headers | Record<string, string> | Array<[string, string]>;
status: number;
statusText: string;
}
// above init is the Web Standards ResponseInit:
type ResponseInit = {
headers?: Headers | Record<string, string> | Array<[string, string]>;
status?: number;
statusText?: string;
};

@@ -244,2 +245,52 @@ // Create a redirect Response:

If you want to manually manage serving a file, you can use the following approach.
```ts
import { HttpRouter, serveFiles } from 'bunshine';
const app = new HttpRouter();
app.get('/assets/:name.png', c => {
const name = c.params.name;
const filePath = `${import.meta.dir}/assets/${name}.png`;
// you can pass a string path
return c.file(filePath);
// Bun will set Content-Type based on the string file extension
});
app.get('/build/:hash.map', c => {
const hash = c.params.hash;
const filePath = `${import.meta.dir}/assets/${name}.png`;
// you can pass a BunFile
return c.file(
Bun.file(filePath, {
// Bun will automatically set Content-Type based on the file's extension
// but you can set it or override it, for instance if Bun doesn't know it's type
headers: { 'Content-type': 'application/json' },
})
);
});
app.get('/profile/*.jpg', async c => {
// you can pass a Buffer, Readable, or TypedArray
const intArray = getBytesFromExternal(c.params[0]);
const resp = c.file(bytes);
// You can use something like file-type on npm
// To get a mime type based on binary data
const { mime } = await fileTypeFromBuffer(intArray);
resp.headers.set('Content-type', mime);
return resp;
});
app.get('/files/*', async c => {
// c.file() accepts 4 options:
return c.file(path, {
disposition, // Use a Content-Disposition header with "inline" or "attachment"
headers, // additional headers to add
acceptRanges, // unless false, will support partial (ranged) downloads
chunkSize, // Size for ranged downloads when client doesn't specify chunk size. Defaults to 3MB
});
});
```
## Writing middleware

@@ -491,10 +542,10 @@

```ts
import { HttpRouter } from 'bunshine';
import { HttpRouter, SocketMessage } from 'bunshine';
const app = new HttpRouter();
// regular routes
// register regular routes on app.get()/post()/etc
app.get('/', c => c.text('Hello World!'));
// WebSocket routes
// Register WebSocket routes with app.socket.at()
type ParamsShape = { room: string };

@@ -504,5 +555,5 @@ type DataShape = { user: User };

// Optional. Allows you to specify arbitrary data to attach to ws.data.
upgrade: sc => {
upgrade: c => {
// upgrade is called on first connection, before HTTP 101 is issued
const cookies = sc.request.headers.get('cookie');
const cookies = c.request.headers.get('cookie');
const user = getUserFromCookies(cookies);

@@ -534,3 +585,3 @@ return { user };

// List of codes and messages: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code
close(sc, code, message) {
close(sc, code, reason) {
const room = sc.params.room;

@@ -563,2 +614,105 @@ const user = sc.data.user;

### Are sockets magical?
They're simpler than you think. What Bun does internally:
- Responds to a regular HTTP request with HTTP 101 (Switching Protocols)
- Tells the client to make socket connection on another port
- Keeps connection open and sends/receives messages
- Keeps objects in memory to represent each connected client
- Tracks topic subscription and un-subscription for each client
In Node, the process is complicated because you have to import and orchestrate http/https, and net.
But Bun provides Bun.serve() which handles everything. Bunshine is a wrapper
around Bun.serve that adds routing and convenience functions.
With Bunshine you don't need to use Socket.IO or any other framework.
Connecting from the client requires no library either. You can simply
create a new WebSocket() object and use it to listen and send data.
For pub-sub, Bun internally handles subscriptions and broadcasts. See below for
pub-sub examples using Bunshine.
### Socket Context properties
```ts
import { Context, HttpRouter, NextFunction, SocketContext } from 'bunshine';
const app = new HttpRouter();
// WebSocket routes
type ParamsShape = { room: string };
type DataShape = { user: User };
app.socket.at<ParmasShape, DataShape>('/games/rooms/:room', {
// Optional. Allows you to specify arbitrary data to attach to ws.data.
upgrade: (c: Context, next: NextFunction) => {
// Functions are type annotated for illustrations, but is not neccessary
// c and next here are the same as regular http endpoings
return data; // data returned becomes available on sc.data
},
// Optional. Called when the client sends a message
message(sc, message) {
sc.url; // the URL object for the request
sc.server; // Same as app.server
sc.params; // Route params for the request (in this case { room: string; })
sc.data; // Any data you return from the upgrade handler
sc.remoteAddress; // the client or load balancer IP Address
sc.readyState; // 0=connecting, 1=connected, 2=closing, 3=close
sc.binaryType; // nodebuffer, arraybuffer, uint8array
sc.send(message, compress /*optional*/); // compress is optional
// message can be string, data to be JSON.stringified, or binary data such as Buffer or Uint8Array.
// compress can be true to compress message
sc.close(status /*optional*/, reason /*optional*/); // status and reason are optional
// status can be a valid WebSocket status number (in the 1000s)
// reason can be text to tell client why you are closing
sc.terminate(); // terminates socket without telling client why
sc.subscribe(topic); // The name of a topic to subscribe this client
sc.unsubscribe(topic); // Name of topic to unsubscribe
sc.isSubscribed(topic); // True if subscribed to that topic name
sc.cork(topic); // Normally there is no reason to use this function
sc.publish(topic, message, compress /*optional*/); // Publish message to all subscribed clients
sc.ping(data /*optional*/); // Tell client to stay connected
sc.pong(data /*optional*/); // Way to respond to client request to stay connected
message.raw(); // get the raw string or Buffer of the message
message.text(encoding /*optional*/); // get message as string
`${message}`; // will do the same as .text()
message.buffer(); // get data as Buffer
message.arrayBuffer(); // get data as array buffer
message.readableStream(); // get data as a ReadableStream object
message.json(); // parse data with JSON.parse()
message.type; // message, ping, or pong
},
// called when a handler throws any error
error: (sc: SocketContext, error: Error) => {
// sc is the same as above
},
// Optional. Called when the client disconnects
// List of codes and messages: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code
close(sc: SocketContext, code: number, reason: string) {
// sc is the same as above
},
});
// start the server
app.listen({ port: 3100, reusePort: true });
//
// Browser side:
//
const gameRoom = new WebSocket('ws://localhost:3100/games/rooms/1?user=42');
gameRoom.onmessage = e => {
// receiving messages
const data = JSON.parse(e.data);
if (data.type === 'GameState') {
setGameState(data);
} else if (data.type === 'GameMove') {
playMove(data);
}
};
gameRoom.onerror = handleGameError;
// send message to server
gameRoom.send(JSON.stringify({ type: 'GameMove', move: 'rock' }));
```
## WebSocket pub-sub

@@ -574,4 +728,2 @@

app.get('/', c => c.text('Hello World!'));
type ParamsShape = { room: string };

@@ -593,7 +745,8 @@ type DataShape = { username: string };

// to each connection's message handler
const fullMessage = `${sc.data.username}: ${message}`;
// so you need to call sc.publish() and sc.send()
const fullMessage = `${sc.data.username}: ${message.text()}`;
sc.publish(`chat-room-${sc.params.room}`, fullMessage);
sc.send(fullMessage);
},
close(sc, code, message) {
close(sc, code, reason) {
const msg = `${sc.data.username} has left the chat`;

@@ -1041,3 +1194,3 @@ sc.publish(`chat-room-${sc.params.room}`, msg);

<img alt="devLogger" src="https://github.com/kensnyder/bunshine/raw/main/assets/devLogger-screenshot.png?v=3.1.0" width="524" height="78" />
<img alt="devLogger" src="https://github.com/kensnyder/bunshine/raw/main/assets/devLogger-screenshot.png?v=3.1.2" width="524" height="78" />

@@ -1058,5 +1211,5 @@ `prodLogger` outputs logs in JSON with the following shape:

"runtime": "Bun v1.1.34",
"poweredBy": "Bunshine v3.1.0",
"poweredBy": "Bunshine v3.1.2",
"machine": "server1",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.1.0.0 Safari/537.36",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
"pid": 123

@@ -1078,5 +1231,5 @@ }

"runtime": "Bun v1.1.34",
"poweredBy": "Bunshine v3.1.0",
"poweredBy": "Bunshine v3.1.2",
"machine": "server1",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.1.0.0 Safari/537.36",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
"pid": 123,

@@ -1235,3 +1388,3 @@ "took": 5

},
close(ws, code, message) {
close(ws, code, reason) {
// TypeScript knows that ws.data.params.room is a string

@@ -1270,5 +1423,23 @@ // TypeScript knows that ws.data.user is a User

for (const [key, value] of c.url.searchParams) {
} // iterate params
// iterate params
}
});
// Or set c.query via middleware
app.use(c => {
c.query = Object.fromEntries(c.url.searchParams);
});
// how to read json payload
app.post('/api/user', async c => {
const data = await c.request.json();
});
// Or set c.body via middleware
app.on(['POST', 'PUT', 'PATCH'], async c => {
if (c.request.headers.get('Content-Type')?.includes('application/json')) {
c.body = await c.request.json();
}
});
// create small functions that always return the same thing

@@ -1280,8 +1451,2 @@ const respondWith404 = c => c.text('Not found', { status: 404 });

app.all(/\.(env|bak|old|tmp|backup|log|ini|conf)$/, respondWith404);
// block WordPress URLs such as /wordpress/wp-includes/wlwmanifest.xml
app.all(/(^wordpress\/|\/wp-includes\/)/, respondWith404);
// block Other language URLs such as /phpinfo.php and /admin.cgi
app.all(/^[^/]+\.(php|cgi)$/, respondWith404);
// block Commonly probed application paths
app.all(/^(phpmyadmin|mysql|cgi-bin|cpanel|plesk)/i, respondWith404);

@@ -1316,3 +1481,3 @@ // middleware to add CSP

// Persist data in c.locals
app.get('/api/*', async (c, next) => {
app.all('/api/*', async (c, next) => {
const authValue = c.request.headers.get('Authorization');

@@ -1332,3 +1497,3 @@ // subsequent handler will have access to this auth information

if (result.error) {
return c.json(result.error, { status: 400 });
return c.text(result.error, { status: 400 });
}

@@ -1344,4 +1509,7 @@ c.locals.safePayload = result.data;

// do stuff with url and request
return json({ message: 'my json response' });
return json({ message: `my json response at ${url.pathname}` });
});
// listen on random port
app.listen({ port: 0, reusePort: true });
```

@@ -1348,0 +1516,0 @@

@@ -197,13 +197,13 @@ import type { ServeOptions, Server } from 'bun';

}
use(...handlers: Handler<{}>[]) {
use = (...handlers: Handler<{}>[]) => {
return this.all('*', handlers);
}
on404(...handlers: SingleHandler<Record<string, string>>[]) {
};
on404 = (...handlers: SingleHandler<Record<string, string>>[]) => {
this._on404Handlers.push(...handlers.flat(9));
return this;
}
on500(...handlers: SingleHandler<Record<string, string>>[]) {
};
on500 = (...handlers: SingleHandler<Record<string, string>>[]) => {
this._on500Handlers.push(...handlers.flat(9));
return this;
}
};
fetch = async (request: Request, server: Server) => {

@@ -210,0 +210,0 @@ const context = new Context(request, server, this);

@@ -10,3 +10,3 @@ export type RangeInformation = {

totalFileSize,
defaultChunkSize = 3 * 1024 ** 2, // 3MB chunk if byte range is open ended
defaultChunkSize = 1024 ** 2, // 1MB chunk if byte range is open ended
}: RangeInformation) {

@@ -17,2 +17,6 @@ if (!rangeHeader) {

}
if (totalFileSize === 0) {
// server should respond with "content-range: bytes */0"
return { slice: null, contentLength: 0, status: 416 };
}
// only support single ranges

@@ -43,5 +47,6 @@ const match = rangeHeader.match(/^bytes=(\d*)-(\d*)$/i);

if (start === 0 && end === totalFileSize - 1) {
return { slice: null, contentLength: totalFileSize, status: 200 };
// safari expects a 206 even if the range is the full file
// return { slice: null, contentLength: totalFileSize, status: 200 };
}
return { slice: { start, end }, contentLength: end - start + 1, status: 206 };
}

@@ -11,2 +11,3 @@ import { BunFile } from 'bun';

acceptRanges?: boolean;
headers?: HeadersInit;
};

@@ -35,4 +36,5 @@

resp.headers.set('Last-Modified', new Date(file.lastModified).toUTCString());
if (fileOptions.disposition === 'attachment') {
const filename = path.basename(file.name!);
// optionally add disposition
if (fileOptions.disposition === 'attachment' && file.name) {
const filename = path.basename(file.name);
resp.headers.set(

@@ -45,2 +47,9 @@ 'Content-Disposition',

}
// optionally add headers
if (fileOptions.headers) {
const headers = new Headers(fileOptions.headers);
for (const [name, value] of Object.entries(headers)) {
resp.headers.set(name, value);
}
}
return resp;

@@ -86,2 +95,3 @@ }

'Content-Length': String(file.size),
// Currently Bun overrides the Content-Length header to be 0
'X-Content-Length': String(file.size),

@@ -88,0 +98,0 @@ ...(acceptRanges ? { 'Accept-Ranges': 'bytes' } : {}),

@@ -150,3 +150,3 @@ import { Server, ServerWebSocket, ServerWebSocketSendStatus } from 'bun';

readableStream(chunkSize: number = 1024) {
// @ts-expect-error
// @ts-expect-error - type is incorrect
return new Blob([this.buffer()]).stream(chunkSize);

@@ -153,0 +153,0 @@ }

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