22
33A Bun HTTP & WebSocket server that is a little ray of sunshine.
44
5- <img alt =" Bunshine Logo " src =" https://github.com/kensnyder/bunshine/raw/main/packages/bunshine/assets/bunshine-logo.png?v=3.1.1 " width =" 200 " height =" 187 " />
5+ <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 " />
66
7- [ ![ NPM Link] ( https://img.shields.io/npm/v/bunshine?v=3.1.1 )] ( https://npmjs.com/package/bunshine )
8- [ ![ Language: TypeScript] ( https://badgen.net/static/language/TS?v=3.1.1 )] ( https://github.com/search?q=repo:kensnyder/bunshine++language:TypeScript&type=code )
9- [ ![ Code Coverage] ( https://codecov.io/gh/kensnyder/bunshine/graph/badge.svg?token=4LLWB8NBNT&v=3.1.1 )] ( https://codecov.io/gh/kensnyder/bunshine )
10- [ ![ Dependencies: 1] ( https://badgen.net/static/dependencies/1/green?v=3.1.1 )] ( https://www.npmjs.com/package/bunshine?activeTab=dependencies )
11- ![ Tree shakeable] ( https://badgen.net/static/tree%20shakeable/yes/green?v=3.1.1 )
12- [ ![ ISC License] ( https://badgen.net/github/license/kensnyder/bunshine?v=3.1.1 )] ( https://opensource.org/licenses/ISC )
7+ [ ![ NPM Link] ( https://img.shields.io/npm/v/bunshine?v=3.1.2 )] ( https://npmjs.com/package/bunshine )
8+ [ ![ Language: TypeScript] ( https://badgen.net/static/language/TS?v=3.1.2 )] ( https://github.com/search?q=repo:kensnyder/bunshine++language:TypeScript&type=code )
9+ [ ![ Code Coverage] ( https://codecov.io/gh/kensnyder/bunshine/graph/badge.svg?token=4LLWB8NBNT&v=3.1.2 )] ( https://codecov.io/gh/kensnyder/bunshine )
10+ [ ![ Dependencies: 1] ( https://badgen.net/static/dependencies/1/green?v=3.1.2 )] ( https://www.npmjs.com/package/bunshine?activeTab=dependencies )
11+ ![ Tree shakeable] ( https://badgen.net/static/tree%20shakeable/yes/green?v=3.1.2 )
12+ [ ![ ISC License] ( https://badgen.net/github/license/kensnyder/bunshine?v=3.1.2 )] ( https://opensource.org/licenses/ISC )
1313
1414## Installation
1515
@@ -61,7 +61,8 @@ _Or to run Bunshine on Node,
616112 . [ Examples of common http server setup] ( #examples-of-common-http-server-setup )
626213 . [ Design Decisions] ( #design-decisions )
636314 . [ Roadmap] ( #roadmap )
64- 15 . [ ISC License] ( ./LICENSE.md )
64+ 15 . [ Change Log] ( ./CHANGELOG.md )
65+ 16 . [ ISC License] ( ./LICENSE.md )
6566
6667## Upgrading from 1.x to 2.x
6768
@@ -238,6 +239,56 @@ See the [serveFiles](#serveFiles) section for more info.
238239Also note you can serve files with Bunshine anywhere with ` bunx bunshine-serve ` .
239240It currently uses the default ` serveFiles() ` options.
240241
242+ If you want to manually manage serving a file, you can use the following approach.
243+
244+ ``` ts
245+ import { HttpRouter , serveFiles } from ' bunshine' ;
246+
247+ const app = new HttpRouter ();
248+
249+ app .get (' /assets/:name.png' , c => {
250+ const name = c .params .name ;
251+ const filePath = ` ${import .meta .dir }/assets/${name }.png ` ;
252+ // you can pass a string path
253+ return c .file (filePath );
254+ // Bun will set Content-Type based on the string file extension
255+ });
256+
257+ app .get (' /build/:hash.map' , c => {
258+ const hash = c .params .hash ;
259+ const filePath = ` ${import .meta .dir }/assets/${name }.png ` ;
260+ // you can pass a BunFile
261+ return c .file (
262+ Bun .file (filePath , {
263+ // Bun will automatically set Content-Type based on the file's extension
264+ // but you can set it or override it, for instance if Bun doesn't know it's type
265+ headers: { ' Content-type' : ' application/json' },
266+ })
267+ );
268+ });
269+
270+ app .get (' /profile/*.jpg' , async c => {
271+ // you can pass a Buffer, Readable, or TypedArray
272+ const intArray = getBytesFromExternal (c .params [0 ]);
273+ const resp = c .file (bytes );
274+ // You can use something like file-type on npm
275+ // To get a mime type based on binary data
276+ const { mime } = await fileTypeFromBuffer (intArray );
277+ resp .headers .set (' Content-type' , mime );
278+ return resp ;
279+ });
280+
281+ app .get (' /files/*' , async c => {
282+ // c.file() accepts 4 options:
283+ return c .file (path , {
284+ disposition , // Use a Content-Disposition header with "inline" or "attachment"
285+ headers , // additional headers to add
286+ acceptRanges , // unless false, will support partial (ranged) downloads
287+ chunkSize , // Size for ranged downloads when client doesn't specify chunk size. Defaults to 3MB
288+ });
289+ });
290+ ```
291+
241292## Writing middleware
242293
243294Here are more examples of attaching middleware.
@@ -601,11 +652,11 @@ app.socket.at<ParmasShape, DataShape>('/games/rooms/:room', {
601652 sc .readyState ; // 0=connecting, 1=connected, 2=closing, 3=close
602653 sc .binaryType ; // nodebuffer, arraybuffer, uint8array
603654 sc .send (message , compress /* optional*/ ); // compress is optional
604- // message can be string, data to be JSON.stringified, or binary data such as Buffer or Uint8Array.
605- // compress can be true to compress message
655+ // message can be string, data to be JSON.stringified, or binary data such as Buffer or Uint8Array.
656+ // compress can be true to compress message
606657 sc .close (status /* optional*/ , reason /* optional*/ ); // status and reason are optional
607- // status can be a valid WebSocket status number (in the 1000s)
608- // reason can be text to tell client why you are closing
658+ // status can be a valid WebSocket status number (in the 1000s)
659+ // reason can be text to tell client why you are closing
609660 sc .terminate (); // terminates socket without telling client why
610661 sc .subscribe (topic ); // The name of a topic to subscribe this client
611662 sc .unsubscribe (topic ); // Name of topic to unsubscribe
@@ -620,8 +671,9 @@ app.socket.at<ParmasShape, DataShape>('/games/rooms/:room', {
620671 ` ${message } ` ; // will do the same as .text()
621672 message .buffer (); // get data as Buffer
622673 message .arrayBuffer (); // get data as array buffer
623- message .readableString (); // get data as a ReadableString object
674+ message .readableStream (); // get data as a ReadableStream object
624675 message .json (); // parse data with JSON.parse()
676+ message .type ; // message, ping, or pong
625677 },
626678 // called when a handler throws any error
627679 error : (sc : SocketContext , error : Error ) => {
@@ -1129,7 +1181,7 @@ example:
11291181
11301182Screenshot:
11311183
1132- <img alt =" devLogger " src =" https://github.com/kensnyder/bunshine/raw/main/assets/devLogger-screenshot.png?v=3.1.1 " width =" 524 " height =" 78 " />
1184+ <img alt =" devLogger " src =" https://github.com/kensnyder/bunshine/raw/main/assets/devLogger-screenshot.png?v=3.1.2 " width =" 524 " height =" 78 " />
11331185
11341186` prodLogger ` outputs logs in JSON with the following shape:
11351187
@@ -1145,7 +1197,7 @@ Request log:
11451197 "method" : " GET" ,
11461198 "pathname" : " /home" ,
11471199 "runtime" : " Bun v1.1.34" ,
1148- "poweredBy" : " Bunshine v3.1.1 " ,
1200+ "poweredBy" : " Bunshine v3.1.2 " ,
11491201 "machine" : " server1" ,
11501202 "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" ,
11511203 "pid" : 123
@@ -1164,7 +1216,7 @@ Response log:
11641216 "method" : " GET" ,
11651217 "pathname" : " /home" ,
11661218 "runtime" : " Bun v1.1.34" ,
1167- "poweredBy" : " Bunshine v3.1.1 " ,
1219+ "poweredBy" : " Bunshine v3.1.2 " ,
11681220 "machine" : " server1" ,
11691221 "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" ,
11701222 "pid" : 123 ,
@@ -1354,7 +1406,25 @@ app.get('/', c => {
13541406 c .url .searchParams ; // URLSearchParams object
13551407 Object .fromEntries (c .url .searchParams ); // as plain object (but repeated keys are dropped)
13561408 for (const [key, value] of c .url .searchParams ) {
1357- } // iterate params
1409+ // iterate params
1410+ }
1411+ });
1412+
1413+ // Or set c.query via middleware
1414+ app .use (c => {
1415+ c .query = Object .fromEntries (c .url .searchParams );
1416+ });
1417+
1418+ // how to read json payload
1419+ app .post (' /api/user' , async c => {
1420+ const data = await c .request .json ();
1421+ });
1422+
1423+ // Or set c.body via middleware
1424+ app .on ([' POST' , ' PUT' , ' PATCH' ], async c => {
1425+ if (c .request .headers .get (' Content-Type' )?.includes (' application/json' )) {
1426+ c .body = await c .request .json ();
1427+ }
13581428});
13591429
13601430// create small functions that always return the same thing
@@ -1363,12 +1433,6 @@ const respondWith404 = c => c.text('Not found', { status: 404 });
13631433app .get (/ ^ \. / , respondWith404 );
13641434// block URLs that end with .env and other dumb endings
13651435app .all (/ \. (env| bak| old| tmp| backup| log| ini| conf)$ / , respondWith404 );
1366- // block WordPress URLs such as /wordpress/wp-includes/wlwmanifest.xml
1367- app .all (/ (^ wordpress\/ | \/ wp-includes\/ )/ , respondWith404 );
1368- // block Other language URLs such as /phpinfo.php and /admin.cgi
1369- app .all (/ ^ [^ /] + \. (php| cgi)$ / , respondWith404 );
1370- // block Commonly probed application paths
1371- app .all (/ ^ (phpmyadmin| mysql| cgi-bin| cpanel| plesk)/ i , respondWith404 );
13721436
13731437// middleware to add CSP
13741438app .use (async (c , next ) => {
@@ -1398,7 +1462,7 @@ app.headGet('/embeds/*', async (c, next) => {
13981462});
13991463
14001464// Persist data in c.locals
1401- app .get (' /api/*' , async (c , next ) => {
1465+ app .all (' /api/*' , async (c , next ) => {
14021466 const authValue = c .request .headers .get (' Authorization' );
14031467 // subsequent handler will have access to this auth information
14041468 c .locals .auth = {
@@ -1413,7 +1477,7 @@ function castSchema(zodSchema: ZodObject): Middleware {
14131477 return async c => {
14141478 const result = zodSchema .safeParse (await c .json ());
14151479 if (result .error ) {
1416- return c .json (result .error , { status: 400 });
1480+ return c .text (result .error , { status: 400 });
14171481 }
14181482 c .locals .safePayload = result .data ;
14191483 };
@@ -1424,8 +1488,11 @@ app.post('/api/users', castSchema(userCreateSchema), createUser);
14241488// Destructure context object
14251489app .get (' /api/*' , async ({ url , request , json }) => {
14261490 // do stuff with url and request
1427- return json ({ message: ' my json response' });
1491+ return json ({ message: ` my json response at ${ url . pathname } ` });
14281492});
1493+
1494+ // listen on random port
1495+ app .listen ({ port: 0 , reusePort: true });
14291496```
14301497
14311498## Design Decisions
0 commit comments