Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/blue-snails-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'tanstack-start-bun-server': major
---

Create the initial tanstack-start-bun-server library for running TanStack Start apps on Bun, with static asset handling, caching support, documentation, and tests.
128 changes: 109 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,124 @@
# tanstack-start-bun-server
# `tanstack-start-bun-server`

TypeScript library built with [Bun](https://bun.sh/), [tsdown](https://tsdown.js.org/), [Biome](https://biomejs.dev/), and [Changesets](https://github.com/changesets/changesets).
Bun-only helpers for serving a TanStack Start production build with:

## Features
- a loaded `server/server.js` handler
- static asset routes from `client/**/*`
- in-memory preloading for small assets
- on-demand disk reads for filtered or oversized assets
- optional ETag and gzip support

- Bun-first dependency management, scripts, and tests
- TypeScript source with dual ESM/CJS output and generated types
- Biome for linting and formatting
- Example library export
- Changesets and GitHub Actions for release automation
## Install

## Installation
```sh
bun add tanstack-start-bun-server
```

## Basic usage

```ts
import { createTanStackStartBunServeConfig } from 'tanstack-start-bun-server'

```bash
bun install
const { fetchHandler, routes } = await createTanStackStartBunServeConfig({
webDistPath: './dist',
})

const server = Bun.serve({
port: 3000,
routes: {
...routes,
'/*': (request) => fetchHandler(request),
},
})

console.log(server.url)
```

## Usage
## Use the default console logger explicitly

```ts
import { greet } from 'tanstack-start-bun-server'
import { createConsoleLogger, createTanStackStartBunServeConfig } from 'tanstack-start-bun-server'

console.log(greet('Seed'))
const { fetchHandler, routes } = await createTanStackStartBunServeConfig({
logger: createConsoleLogger(),
webDistPath: './dist',
})
```

## Use a custom logger

```ts
import { createTanStackStartBunServeConfig, type Logger } from 'tanstack-start-bun-server'

const logger: Logger = {
debug(message, ...args) {
console.debug(message, ...args)
},
error(message, ...args) {
console.error(message, ...args)
},
info(message, ...args) {
console.info(message, ...args)
},
}

const { fetchHandler, routes } = await createTanStackStartBunServeConfig({
logger,
maxPreloadBytes: 5 * 1024 * 1024,
webDistPath: './dist',
})
```

## Customize gzip mime types

```ts
import { DEFAULT_GZIP_MIME_TYPES, createTanStackStartBunServeConfig } from 'tanstack-start-bun-server'

const { fetchHandler, routes } = await createTanStackStartBunServeConfig({
gzipMimeTypes: [...DEFAULT_GZIP_MIME_TYPES, 'application/wasm'].sort(),
webDistPath: './dist',
})
```

```ts
import { DEFAULT_GZIP_MIME_TYPES, createTanStackStartBunServeConfig } from 'tanstack-start-bun-server'

const { fetchHandler, routes } = await createTanStackStartBunServeConfig({
gzipMimeTypes: DEFAULT_GZIP_MIME_TYPES.filter((type) => type !== 'image/svg+xml'),
webDistPath: './dist',
})
```

## Use alongside application routes

```ts
import { createTanStackStartBunServeConfig } from 'tanstack-start-bun-server'

const app = {
fetch(request: Request) {
return new Response(`api:${new URL(request.url).pathname}`)
},
}

const { fetchHandler, routes } = await createTanStackStartBunServeConfig({
includePatterns: ['*.css', '*.js', '*.woff2'],
webDistPath: './dist',
})

Bun.serve({
port: 3000,
routes: {
'/api': (request) => app.fetch(request),
'/api/*': (request) => app.fetch(request),
...routes,
'/*': (request) => fetchHandler(request),
},
})
```

## Development

```bash
```sh
bun run build
bun run check-types
bun run lint
Expand All @@ -37,14 +129,12 @@ bun run test:watch

## Publishing

Use Changesets to manage versioning and releases.

```bash
```sh
bun run changeset
bun run version
bun run release
```

## License

MIT see [LICENSE](./LICENSE).
MIT; see [LICENSE](./LICENSE).
4 changes: 1 addition & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@
"url": "https://github.com/beeman/tanstack-start-bun-server/issues"
},
"dependencies": {},
"description": "TypeScript library built with Bun, tsdown, Biome, and Changesets.",
"description": "Bun server helpers for serving TanStack Start production builds",
"devDependencies": {
"@biomejs/biome": "2.4.6",
"@changesets/cli": "2.30.0",
"@intellectronica/ruler": "0.3.38",
"@types/bun": "1.3.11",
"pkg-pr-new": "0.0.62",
"publint": "0.3.18",
"tsdown": "0.21.7"
"tsdown": "0.21.7",
"typescript": "6.0.2"
},
"exports": {
".": {
"bun": "./src/index.ts",
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
Expand All @@ -26,16 +28,14 @@
}
},
"files": [
"dist"
"dist",
"src"
],
"homepage": "https://github.com/beeman/tanstack-start-bun-server#readme",
"license": "MIT",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"name": "tanstack-start-bun-server",
"peerDependencies": {
"typescript": "^5.0.0 || ^6.0.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/beeman/tanstack-start-bun-server.git"
Expand All @@ -53,6 +53,7 @@
"test:watch": "bun test --watch",
"version": "changeset version && bun lint:fix"
},
"sideEffects": false,
"type": "module",
"types": "./dist/index.d.mts",
"version": "0.0.0"
Expand Down
80 changes: 78 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,79 @@
export function greet(name: string = 'World'): string {
return `Hello, ${name} from tanstack-start-bun-server!`
import { createConsoleLogger, type Logger } from './logger.ts'
import { initializeServerHandler } from './server-handler.ts'
import { initializeStaticRoutes, type StaticAssetRouteHandler } from './static-assets.ts'

export const DEFAULT_GZIP_MIME_TYPES = [
'application/javascript',
'application/json',
'application/xml',
'image/svg+xml',
'text/',
] as const

export type { Logger } from './logger.ts'
export { createConsoleLogger }

export interface TanStackStartBunServeConfig {
fetchHandler: (request: Request) => Promise<Response>
routes: Record<string, StaticAssetRouteHandler>
}

export interface TanStackStartBunServeOptions {
enableEtag?: boolean
enableGzip?: boolean
excludePatterns?: string[]
gzipMimeTypes?: string[]
gzipMinBytes?: number
includePatterns?: string[]
logger?: Logger
maxPreloadBytes?: number
verbose?: boolean
webDistPath: string
}

export async function createTanStackStartBunServeConfig({
enableEtag = true,
enableGzip = true,
excludePatterns = [],
gzipMimeTypes = [...DEFAULT_GZIP_MIME_TYPES],
gzipMinBytes = 1024,
includePatterns = [],
logger = createConsoleLogger(),
maxPreloadBytes = 5 * 1024 * 1024,
verbose = false,
webDistPath,
}: TanStackStartBunServeOptions): Promise<TanStackStartBunServeConfig> {
logger.debug('Starting web server')

const handler = await initializeServerHandler({
logger,
webDistPath,
})

logger.debug('TanStack Start application handler initialized')

const { routes } = await initializeStaticRoutes({
enableEtag,
enableGzip,
excludePatterns,
gzipMimeTypes,
gzipMinBytes,
includePatterns,
logger,
maxPreloadBytes,
verbose,
webDistPath,
})

return {
fetchHandler: async (request: Request) => {
try {
return await handler.fetch(request)
} catch (error) {
logger.error(error as Error)
return new Response('Internal Server Error', { status: 500 })
}
},
routes,
}
}
19 changes: 19 additions & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface Logger {
debug(message: unknown, ...args: unknown[]): void
error(message: unknown, ...args: unknown[]): void
info(message: unknown, ...args: unknown[]): void
}

export function createConsoleLogger(): Logger {
return {
debug(message, ...args) {
console.log('DBG', message, ...args)
},
error(message, ...args) {
console.error('ERR', message, ...args)
},
info(message, ...args) {
console.log('INF', message, ...args)
},
}
}
38 changes: 38 additions & 0 deletions src/server-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { join, relative } from 'node:path'
import type { Logger } from './logger.ts'

export interface ServerHandler {
fetch: (request: Request) => Response | Promise<Response>
}

export interface InitializeServerHandlerOptions {
logger: Logger
webDistPath: string
}

export async function initializeServerHandler({
logger,
webDistPath,
}: InitializeServerHandlerOptions): Promise<ServerHandler> {
const serverEntryPoint = join(webDistPath, 'server/server.js')
const displayPath = relative(process.cwd(), serverEntryPoint) || serverEntryPoint

if (!(await Bun.file(serverEntryPoint).exists())) {
logger.info(`Skipping web bootstrap because ${displayPath} is missing`)
return {
fetch: async () => new Response('Not Found', { status: 404 }),
}
}

logger.info(`Loading application handler ${displayPath}`)

try {
const serverModule = (await import(serverEntryPoint)) as {
default: ServerHandler
}

return serverModule.default
} catch (error) {
throw new Error(`Failed to load server handler: ${String(error)}`)
}
}
Loading
Loading