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.2 to 3.2.0

coverage/.lcov.info.00b92c91a77c10a1.tmp

9

CHANGELOG.md
# Changelog
## v3.2.0 - Nov xx, 2024
- Auto-detect file mime type with c.file()
- Properly match fixed paths
- Fix TypeScript types for HTTPS configuration
- Support If-Modified-Since header
- Tweaks to range header handling
## v3.1.2 - Nov 25, 2024

@@ -7,3 +15,2 @@

- Support passing headers to c.file()
- Return 206 in ranged downloads even if whole file is requested (Safari bug)

@@ -10,0 +17,0 @@ ## v3.1.1 - Nov 23, 2024

13

package.json
{
"name": "bunshine",
"version": "3.1.2",
"module": "server/server.ts",
"version": "3.2.0",
"module": "index.ts",
"type": "module",

@@ -48,14 +48,15 @@ "main": "index.ts",

"dependencies": {
"file-type": "19.6.0",
"lru-cache": "11.0.2"
},
"devDependencies": {
"@types/bun": "1.1.13",
"@types/bun": "1.1.14",
"eventsource": "2.0.2",
"prettier": "3.3.3",
"prettier": "3.4.1",
"prettier-plugin-organize-imports": "4.1.0",
"redos-detector": "5.1.3",
"tinybench": "3.0.6",
"type-fest": "4.27.0",
"typescript": "5.6.3"
"type-fest": "4.29.0",
"typescript": "5.7.2"
}
}

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

<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" />
<img alt="Bunshine Logo" src="https://github.com/kensnyder/bunshine/raw/main/packages/bunshine/assets/bunshine-logo.png?v=3.2.0" width="200" height="187" />
[![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)
[![NPM Link](https://img.shields.io/npm/v/bunshine?v=3.2.0)](https://npmjs.com/package/bunshine)
[![Language: TypeScript](https://badgen.net/static/language/TS?v=3.2.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.2.0)](https://codecov.io/gh/kensnyder/bunshine)
![Tree shakeable](https://badgen.net/static/tree%20shakeable/yes/green?v=3.2.0)
[![ISC License](https://badgen.net/github/license/kensnyder/bunshine/packages/bunshine?v=3.2.0)](https://opensource.org/licenses/ISC)

@@ -44,10 +43,11 @@ ## Installation

2. [Full example](#full-example)
3. [Serving static files](#serving-static-files)
4. [Writing middleware](#writing-middleware)
5. [Throwing responses](#throwing-responses)
6. [WebSockets](#websockets)
7. [WebSocket pub-sub](#websocket-pub-sub)
8. [Server Sent Events](#server-sent-events)
9. [Route Matching](#route-matching)
10. [Included middleware](#included-middleware)
3. [SSL](#ssl)
4. [Serving static files](#serving-static-files)
5. [Writing middleware](#writing-middleware)
6. [Throwing responses](#throwing-responses)
7. [WebSockets](#websockets)
8. [WebSocket pub-sub](#websocket-pub-sub)
9. [Server Sent Events](#server-sent-events)
10. [Route Matching](#route-matching)
11. [Included middleware](#included-middleware)
- [serveFiles](#servefiles)

@@ -62,8 +62,8 @@ - [compression](#compression)

- [Recommended Middleware](#recommended-middleware)
11. [TypeScript pro-tips](#typescript-pro-tips)
12. [Examples of common http server setup](#examples-of-common-http-server-setup)
13. [Design Decisions](#design-decisions)
14. [Roadmap](#roadmap)
15. [Change Log](./CHANGELOG.md)
16. [ISC License](./LICENSE.md)
12. [TypeScript pro-tips](#typescript-pro-tips)
13. [Examples of common http server setup](#examples-of-common-http-server-setup)
14. [Design Decisions](#design-decisions)
15. [Roadmap](#roadmap)
16. [Change Log](./CHANGELOG.md)
17. [ISC License](./LICENSE.md)

@@ -224,6 +224,32 @@ ## Upgrading from 1.x to 2.x

## SSL
Supporting HTTPS is simple on Bun and Bunshine. For details on all supported
options, see the [Bun docs on TLS](https://bun.sh/docs/api/http#tls).
You can obtain free SSL certificates from a service such as
[Let's Encrypt](https://letsencrypt.org/getting-started/).
```ts
import { HttpRouter } from 'bunshine';
const app = new HttpRouter();
const server = app.listen({
port: 443, // works on any port
reusePort: true,
tls: {
key: Bun.file(`${import.meta.dir}/certs/my.key`),
cert: Bun.file(`${import.meta.dir}/certs/my.crt`),
ca: Bun.file(`${import.meta.dir}/certs/ca.pem`), // optional
},
});
app.get('/', () => new Response('hello from https'));
const resp = await fetch('https://localhost');
```
## Serving static files
Serving static files is easy with the `serveFiles` middleware. Note that ranged
requests are supported, so you can use this for video streaming or partial
requests are supported, so you can use Bunshine for video streaming and partial
downloads.

@@ -267,4 +293,4 @@

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
// Bunshine will automatically set Content-Type based on the file bytes
// but you can set it or override it for non-standard mime types
headers: { 'Content-type': 'application/json' },

@@ -276,10 +302,5 @@ })

app.get('/profile/*.jpg', async c => {
// you can pass a Buffer, Readable, or TypedArray
// you can pass a Buffer or Uint8Array
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;
return c.file(bytes);
});

@@ -293,3 +314,4 @@

acceptRanges, // unless false, will support partial (ranged) downloads
chunkSize, // Size for ranged downloads when client doesn't specify chunk size. Defaults to 3MB
sendLastModified, // unless false, will report file modification date (For paths or BunFile objects)
chunkSize, // Size for ranged downloads when client doesn't specify chunk size. Defaults to 1MB
});

@@ -327,3 +349,3 @@ });

}
// return the response from the other handlers
// pass the response to other handlers
return resp;

@@ -363,5 +385,2 @@ });

// handler affected by middleware defined above
app.get('/', c => c.text('Hello World!'));
// define a handler function to be used in multiple places

@@ -501,3 +520,3 @@ const ensureSafeData = async (_, next) => {

app.get('/posts', middleware1, middleware2, handler);
app.get('/users', [middleware1, middleware2, handler]);
app.get('/users', [middleware1, middleware2], handler);
app.get('/visitors', [[middleware1, [middleware2, handler]]]);

@@ -545,3 +564,4 @@

Setting up websockets at various paths is easy with the `socket` property.
Setting up websockets is easy by registering handlers for one or more routes
using the `app.socket.at()` function.

@@ -628,12 +648,14 @@ ```ts

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.
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.
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.
Bun also includes built-in subscription and broadcasting for pub-sub
applications. See [below](#websocket-pub-sub) for pub-sub examples using
Bunshine.

@@ -803,4 +825,5 @@ ### Socket Context properties

Note that with SSE, the client must ultimately decide when to stop listening.
Creating an `EventSource` object will open a connection to the server, and if
the server closes the connection, a browser will automatically reconnect.
Creating an `EventSource` object in the browser will open a connection to the
server, and if the server closes the connection, a browser will automatically
reconnect.

@@ -1197,3 +1220,3 @@ So if you want to tell the browser you are done sending events, send a

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

@@ -1214,3 +1237,3 @@ `prodLogger` outputs logs in JSON with the following shape:

"runtime": "Bun v1.1.34",
"poweredBy": "Bunshine v3.1.2",
"poweredBy": "Bunshine v3.2.0",
"machine": "server1",

@@ -1234,3 +1257,3 @@ "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",

"runtime": "Bun v1.1.34",
"poweredBy": "Bunshine v3.1.2",
"poweredBy": "Bunshine v3.2.0",
"machine": "server1",

@@ -1404,3 +1427,3 @@ "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",

```ts
import { HttpRouter, type Middleware } from 'bunshine';
import { type Middleware } from 'bunshine';

@@ -1569,4 +1592,4 @@ function myMiddleware(options: Options): Middleware {

- ✅ Context
- ✅ examples/kitchen-sink.ts
- 🔲 more examples
- ✅ ./examples/kitchen-sink.ts
- 🔲 more examples in ./examples
- ✅ middleware > compression

@@ -1591,3 +1614,3 @@ - ✅ middleware > cors

- 🔲 example of mini app that uses bin/serve.ts (maybe our own docs?)
- 🔲 GitHub Actions to run tests and coverage
- ✅ GitHub Actions to run tests and coverage
- ✅ Replace "ms" with a small and simple implementation

@@ -1594,0 +1617,0 @@

@@ -1,5 +0,6 @@

import type { BunFile, Server } from 'bun';
import type { Server } from 'bun';
import type HttpRouter from '../HttpRouter/HttpRouter';
import factory from '../responseFactories/factory/factory';
import file, { type FileResponseOptions } from '../responseFactories/file/file';
import { FileLike } from '../responseFactories/file/file-io';
import json from '../responseFactories/json/json';

@@ -82,6 +83,6 @@ import redirect from '../responseFactories/redirect/redirect';

file = async (
filenameOrBunFile: string | BunFile,
pathOrData: FileLike,
fileOptions: FileResponseOptions = {}
) => {
return file.call(this, filenameOrBunFile, {
return file.call(this, pathOrData, {
range: this.request.headers.get('Range') || undefined,

@@ -88,0 +89,0 @@ ...fileOptions,

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

import type { ServeOptions, Server } from 'bun';
import { Server, TLSServeOptions } from 'bun';
import os from 'node:os';

@@ -27,3 +27,5 @@ import bunshinePkg from '../../package.json' assert { type: 'json' };

export type ListenOptions = Omit<ServeOptions, 'fetch' | 'websocket'> | number;
export type ListenOptions =
| Omit<TLSServeOptions, 'fetch' | 'websocket'>
| number;

@@ -104,3 +106,3 @@ export type HttpMethods =

}
getExport(options: Omit<ServeOptions, 'fetch' | 'websocket'> = {}) {
getExport(options: Omit<TLSServeOptions, 'fetch' | 'websocket'> = {}) {
const config = {

@@ -110,3 +112,3 @@ port: 0,

fetch: this.fetch,
} as ServeOptions;
} as TLSServeOptions;
if (this._wsRouter) {

@@ -113,0 +115,0 @@ // @ts-expect-error

@@ -45,6 +45,5 @@ export type RangeInformation = {

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

@@ -1,50 +0,60 @@

import { BunFile } from 'bun';
import path from 'node:path';
import Context from '../../Context/Context';
import getMimeType from '../../getMimeType/getMimeType';
import parseRangeHeader from '../../parseRangeHeader/parseRangeHeader';
import {
FileLike,
getFileBaseName,
getFileChunk,
getFileFull,
getFileMime,
getFileStats,
isFileLike,
} from './file-io';
import isModifiedSince from './isModifiedSince';
export type FileResponseOptions = {
chunkSize?: number;
disposition?: 'inline' | 'attachment';
disposition?: 'inline' | 'attachment' | 'form-data';
acceptRanges?: boolean;
sendLastModified?: boolean;
headers?: HeadersInit;
};
const headersWeAdd = [
'content-type',
'content-length',
'x-content-length',
'content-range',
'accept-ranges',
'last-modified',
'content-disposition',
];
export default async function file(
this: Context,
filenameOrBunFile: string | BunFile,
fileLike: FileLike,
fileOptions: FileResponseOptions = {}
) {
let file =
typeof filenameOrBunFile === 'string'
? Bun.file(filenameOrBunFile)
: filenameOrBunFile;
if (!(await file.exists())) {
return new Response('File not found', { status: 404 });
const resp = await getFileResponse(this.request, fileLike, fileOptions);
if (fileOptions.disposition && /^attachment$/.test(fileOptions.disposition)) {
const filename = getFileBaseName(fileLike);
let disposition = 'attachment';
if (filename) {
disposition += `; filename="${filename}"`;
}
resp.headers.set('Content-Disposition', disposition);
} else if (
fileOptions.disposition &&
/^inline|form-data$/.test(fileOptions.disposition)
) {
resp.headers.set('Content-Disposition', fileOptions.disposition);
}
const resp = await buildFileResponse({
file,
acceptRanges: fileOptions.acceptRanges !== false,
chunkSize: fileOptions.chunkSize,
rangeHeader: this.request.headers.get('Range'),
method: this.request.method,
});
// add last modified
resp.headers.set('Last-Modified', new Date(file.lastModified).toUTCString());
// optionally add disposition
if (fileOptions.disposition === 'attachment' && file.name) {
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');
}
// 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);
for (const [name, value] of headers.entries()) {
if (headersWeAdd.includes(name.toLowerCase())) {
resp.headers.set(name, value);
} else {
resp.headers.append(name, value);
}
}

@@ -55,61 +65,90 @@ }

async function buildFileResponse({
file,
acceptRanges,
chunkSize,
rangeHeader,
method,
}: {
file: BunFile;
acceptRanges: boolean;
chunkSize?: number;
rangeHeader?: string | null;
method: string;
}) {
const { slice, status } = parseRangeHeader({
rangeHeader: acceptRanges ? rangeHeader : null,
totalFileSize: file.size,
defaultChunkSize: chunkSize,
});
if (status === 416) {
return new Response(
method === 'HEAD'
? ''
: `Requested range is not satisfiable. Total size is ${file.size} bytes.`,
{
status: 416,
headers: {
'Content-Range': `bytes */${file.size}`,
},
}
);
async function getFileResponse(
request: Request,
file: FileLike,
fileOptions: FileResponseOptions
) {
if (!file || !isFileLike(file)) {
return new Response('File not found', { status: 404 });
}
if (method === 'HEAD') {
const { size, lastModified, doesExist } = await getFileStats(file);
if (!doesExist) {
return new Response('File not found', { status: 404 });
}
if (lastModified instanceof Date && !isModifiedSince(request, lastModified)) {
return new Response(null, { status: 304 });
}
const supportRangedRequest = fileOptions.acceptRanges !== false;
const maybeModifiedHeader: ResponseInit['headers'] =
lastModified instanceof Date && fileOptions.sendLastModified !== false
? { 'Last-Modified': lastModified.toUTCString() }
: {};
const maybeAcceptRangesHeader: ResponseInit['headers'] = supportRangedRequest
? { 'Accept-Ranges': 'bytes' }
: {};
if (request.method === 'HEAD') {
const mime = await getFileMime(file);
return new Response(null, {
status,
status: 200,
headers: {
'Content-Type': getMimeType(file),
'Content-Length': String(file.size),
'Content-Type': mime,
'Content-Length': String(size),
...maybeModifiedHeader,
...maybeAcceptRangesHeader,
// Currently Bun overrides the Content-Length header to be 0
'X-Content-Length': String(file.size),
...(acceptRanges ? { 'Accept-Ranges': 'bytes' } : {}),
// see https://github.com/oven-sh/bun/issues/15355
'X-Content-Length': String(size),
},
});
}
let buffer = await file.arrayBuffer();
if (slice) {
buffer = buffer.slice(slice.start, slice.end + 1);
const rangeHeader = request.headers.get('Range');
if (supportRangedRequest && rangeHeader) {
const { slice, status } = parseRangeHeader({
rangeHeader: rangeHeader,
totalFileSize: size || 0,
defaultChunkSize: fileOptions.chunkSize,
});
if (status === 416) {
return new Response(
`Requested range is not satisfiable. Total size is ${size} bytes.`,
{
status: 416,
headers: {
'Content-Type': await getFileMime(file),
'Content-Range': `bytes */${size}`,
...maybeModifiedHeader,
'Accept-Ranges': 'bytes',
// Content-length set automatically based on the string length of error message
},
}
);
}
if (slice) {
const buffer = await getFileChunk(
file,
slice.start,
slice.end - slice.start + 1
);
return new Response(buffer, {
status: 206,
headers: {
'Content-Type': await getFileMime(file),
// Content-length will get sent automatically
...maybeModifiedHeader,
'Content-Range': `bytes ${slice.start}-${slice.end}/${size}`,
'Accept-Ranges': 'bytes',
},
});
}
}
const buffer = await getFileFull(file);
return new Response(buffer, {
status,
status: 200,
headers: {
'Content-Type': getMimeType(file),
'Content-Length': String(buffer.byteLength),
'X-Content-Length': String(buffer.byteLength),
...(slice
? { 'Content-Range': `bytes ${slice.start}-${slice.end}/${file.size}` }
: {}),
...(acceptRanges ? { 'Accept-Ranges': 'bytes' } : {}),
'Content-Type': await getFileMime(file, buffer),
// Content-length will get sent automatically
...maybeModifiedHeader,
...maybeAcceptRangesHeader,
},
});
}

@@ -88,3 +88,3 @@ type Registration<T> = {

pattern,
regex: new RegExp(`^${pattern}$`),
regex: new RegExp(`^${regexEsc(pattern)}$`),
matcher: subject => (subject === pattern ? {} : null),

@@ -91,0 +91,0 @@ target,

Sorry, the diff of this file is not supported yet

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