mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 22:01:47 +00:00
Compare commits
22 Commits
claude/fix
...
rfc/bun-bu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84d5794477 | ||
|
|
d3d22dc4f7 | ||
|
|
21cb257651 | ||
|
|
2fbe8ba36d | ||
|
|
55dbe2a79a | ||
|
|
ab345d110a | ||
|
|
e0b01701df | ||
|
|
5e86db7911 | ||
|
|
c82345d0ff | ||
|
|
c52ca33443 | ||
|
|
9abdf1b06c | ||
|
|
a9f0df0a06 | ||
|
|
2319ab2232 | ||
|
|
c6b9774c42 | ||
|
|
d3dad085f3 | ||
|
|
f49e30ddc3 | ||
|
|
3d5312ca4a | ||
|
|
629317b642 | ||
|
|
7c2bb689cd | ||
|
|
26011ba009 | ||
|
|
bbe2a638b6 | ||
|
|
8ef998bfd9 |
@@ -1,4 +1,28 @@
|
||||
# RFCs
|
||||
|
||||
| Number | Name | Issue |
|
||||
| ------ | ---- | ----- |
|
||||
| Number | Name | Issue |
|
||||
| ------ | --------------- | ----------------------------- |
|
||||
| 1 | `Bun.build` API | (`Bun.build`)[./bun-build.ts] |
|
||||
| 2 | `Bun.App` API | (`Bun.App`)[./bun-app.ts] |
|
||||
|
||||
### #1 `Bun.build()`
|
||||
|
||||
The spec for bundler configuration object is defined in [`bun-build-config.ts`][./bun-bundler-config.ts]. These config objects are shared between two proposed APIs:
|
||||
|
||||
- `class` [`Bun.Bundler`][./bun-bundler.ts]
|
||||
|
||||
### #2 `Bun.App()`
|
||||
|
||||
A class for orchestrating builds & HTTP. This class is a layer that sits on top of the `Bun.build` and `Bun.serve`, intended primarily for use by framework authors.
|
||||
|
||||
- `class` [`Bun.App`][./bun-app.ts]: other possible names: `Bun.Builder`, `Bun.Engine`, `Bun.Framework`
|
||||
|
||||
High-level: an `App` consists of a set of _bundlers_ and _routers_. During build/serve, Bun will:
|
||||
|
||||
- iterate over all routers
|
||||
- each router specifies a bundler configuration (the `build` key) and an `entrypoint`/`dir`
|
||||
- if dir, all files in entrypoint are considered entrypoints
|
||||
- everything is bundled
|
||||
- the built results are served over HTTP
|
||||
- each router has a route `prefix` from which its build assets are served
|
||||
- for "mode: handler", the handler is loaded and called instead of served as a static asset
|
||||
|
||||
274
docs/rfcs/bun-app.tsx
Normal file
274
docs/rfcs/bun-app.tsx
Normal file
@@ -0,0 +1,274 @@
|
||||
import { FileSystemRouter, MatchedRoute, ServeOptions, Server } from "bun";
|
||||
|
||||
import { BuildManifest, BuildConfig } from "./bun-build-config";
|
||||
import { BuildResult } from "./bun-build";
|
||||
|
||||
interface AppConfig {
|
||||
configs: Array<Omit<BuildConfig, "entrypoints"> & { name: string }>;
|
||||
routers: Array<AppServeRouter>;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Bun.App
|
||||
*
|
||||
* On build/serve:
|
||||
* - iterate over all routers
|
||||
* - each router specifies either an `entrypoint`/`dir` & build config
|
||||
* - if dir, all files in entrypoint are considered entrypoints
|
||||
* - everything is built
|
||||
* - the built results are served over HTTP
|
||||
* - each router has a route `prefix` from which its build assets are served
|
||||
* - for "mode: handler", the handler is loaded and called instead of served as a static asset
|
||||
*/
|
||||
|
||||
type AppServeRouter =
|
||||
| {
|
||||
// handler mode
|
||||
mode: "static";
|
||||
// directory to serve from
|
||||
// e.g. "./public"
|
||||
dir: string;
|
||||
// specify build to use
|
||||
// no "building" happens with mode static, but
|
||||
// this is needed to know the outdir
|
||||
build: string;
|
||||
// serve these files at a path
|
||||
// e.g. "/static"
|
||||
prefix?: string;
|
||||
|
||||
// only required in "handler" mode
|
||||
handler?: string;
|
||||
}
|
||||
| {
|
||||
// serve the build outputs of a given build
|
||||
mode: "build";
|
||||
dir: string;
|
||||
// must match a `name` specified in one of the `AppConfig`s
|
||||
// serve the build outputs of the build
|
||||
// with the given name
|
||||
build: string;
|
||||
// serve these files at a path
|
||||
// e.g. "/static"
|
||||
prefix?: string;
|
||||
// whether to serve entrypoints using their original names
|
||||
// e.g. "index.tsx" instead of "index-[hash].js"
|
||||
preserveNames?: boolean;
|
||||
}
|
||||
| {
|
||||
mode: "handler";
|
||||
// path to file that `export default`s a handler
|
||||
// this file is automatically added as an entrypoint in the build
|
||||
// e.g. ./serve.tsx
|
||||
handler: string;
|
||||
// router info - this is optional
|
||||
// not necessary for simple handlers
|
||||
// if a route is matched, the handler is called
|
||||
// the MatchedRoute is passed as context.match
|
||||
style?: "static" | "nextjs";
|
||||
// request prefix, e.g. "/static"
|
||||
// if incoming Request doesn't match prefix, no JS runs
|
||||
dir?: string;
|
||||
// handle requests that match this prefix
|
||||
// e.g. /api
|
||||
prefix?: string;
|
||||
// what config to use for to build the matched file
|
||||
// e.g. "client"
|
||||
build: string;
|
||||
|
||||
// whether to provide a build manifest in context.handler
|
||||
// default true
|
||||
manifest?: boolean;
|
||||
// whether to parse query params
|
||||
// provided as context.queryParams
|
||||
queryParams?: boolean;
|
||||
};
|
||||
|
||||
export declare class App {
|
||||
// you can a BuildConfig of an array of BuildConfigs
|
||||
// elements of the array can be undefined to make conditional builds easier
|
||||
/**
|
||||
*
|
||||
*
|
||||
* new App([
|
||||
* { ... },
|
||||
* condition ? {} : undefined
|
||||
* ])
|
||||
*/
|
||||
constructor(options: AppConfig);
|
||||
// run a build and start the dev server
|
||||
serve(options: Partial<ServeOptions>): Promise<Server>;
|
||||
// run full build
|
||||
build(options?: {
|
||||
// all output directories are specified in `AppBuildConfig`
|
||||
// the `write` flag determines whether the build is written to disk
|
||||
// if write = true, the Blobs are BunFile
|
||||
// if write = false, the Blobs are just Blobs
|
||||
write?: boolean;
|
||||
}): Promise<BuildResult<Blob>>;
|
||||
|
||||
handle(req: Request): Promise<Response | null>;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////
|
||||
/////////////////////////////////////////
|
||||
///////// HANDLER SPEC //////////
|
||||
/////////////////////////////////////////
|
||||
/////////////////////////////////////////
|
||||
interface Handler {
|
||||
default: (req: Request, context: HandlerContext) => Promise<Response | null>;
|
||||
// optional function that returns a list of imports
|
||||
// these modules are loaded synchronously by Bun
|
||||
// and passed into handler as context.imports
|
||||
getImports?: (context: HandlerContext) => Import[];
|
||||
}
|
||||
|
||||
type Import = { names: { [k: string]: string }; from: string };
|
||||
|
||||
// the data that is passed as context to the Request handler
|
||||
// - manifest
|
||||
// - match: MatchedResult, only provided if `match` is specified in the `AppConfig`
|
||||
// - imports: only provided if `getImports` is specified in the `Handler`
|
||||
|
||||
interface HandlerContext {
|
||||
manifest?: BuildManifest;
|
||||
match?: MatchedRoute;
|
||||
imports: unknown; // depends on result of `getImports`
|
||||
}
|
||||
|
||||
/////////////////////////////////////
|
||||
/////////////////////////////////////
|
||||
///////// EXAMPLES //////////
|
||||
/////////////////////////////////////
|
||||
/////////////////////////////////////
|
||||
|
||||
// simple static file server
|
||||
{
|
||||
const server = new App({
|
||||
configs: [
|
||||
{
|
||||
name: "static-server",
|
||||
outdir: "./out",
|
||||
},
|
||||
],
|
||||
routers: [
|
||||
{
|
||||
// this adds every file in `./public` as an "entrypoint"
|
||||
mode: "static",
|
||||
dir: "./public",
|
||||
build: "static-server",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// serves files from `./public` on port 3000
|
||||
await server.serve({
|
||||
port: 3000,
|
||||
});
|
||||
|
||||
// copies files from ./public to `.build/client`
|
||||
await server.build();
|
||||
}
|
||||
|
||||
// simple API server
|
||||
{
|
||||
/////////////////
|
||||
// handler.tsx //
|
||||
/////////////////
|
||||
// @ts-ignore
|
||||
export default (req: Request, ctx: BuildContext) => {
|
||||
return new Response("hello world");
|
||||
};
|
||||
|
||||
/////////////
|
||||
// app.tsx //
|
||||
/////////////
|
||||
const app = new App({
|
||||
configs: [
|
||||
{
|
||||
name: "simple-http",
|
||||
target: "bun",
|
||||
outdir: "./.build/server",
|
||||
// bundler config...
|
||||
},
|
||||
],
|
||||
routers: [
|
||||
{
|
||||
mode: "handler",
|
||||
handler: "./handler.tsx", // automatically included as entrypoint
|
||||
prefix: "/api",
|
||||
build: "simple-http",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
app.serve({
|
||||
port: 3000,
|
||||
});
|
||||
}
|
||||
|
||||
// SSR react, pages directory
|
||||
{
|
||||
/////////////////
|
||||
// handler.tsx //
|
||||
/////////////////
|
||||
// @ts-ignore
|
||||
import { renderToReadableStream } from "react-dom/server";
|
||||
|
||||
// @ts-ignore
|
||||
export default (req: Request, context: HandlerContext) => {
|
||||
const { manifest } = context;
|
||||
const { default: Page } = await import(context.match!.filePath);
|
||||
const stream = renderToReadableStream(<Page />, {
|
||||
// get path to client build for hydration
|
||||
bootstrapModules: [manifest?.inputs["./client-entry.tsx"].output.path],
|
||||
});
|
||||
return new Response(stream);
|
||||
};
|
||||
|
||||
/////////////
|
||||
// app.tsx //
|
||||
/////////////
|
||||
const projectRoot = process.cwd();
|
||||
const app = new App({
|
||||
configs: [
|
||||
{
|
||||
name: "react-ssr",
|
||||
target: "bun",
|
||||
outdir: "./.build/server",
|
||||
// bundler config
|
||||
},
|
||||
{
|
||||
name: "react-client",
|
||||
target: "browser",
|
||||
outdir: "./.build/client",
|
||||
transform: {
|
||||
exports: {
|
||||
pick: ["default"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
routers: [
|
||||
{
|
||||
mode: "handler",
|
||||
handler: "./handler.tsx",
|
||||
build: "react-ssr",
|
||||
style: "nextjs",
|
||||
dir: projectRoot + "/pages",
|
||||
},
|
||||
{
|
||||
mode: "build",
|
||||
build: "react-client",
|
||||
dir: "./pages",
|
||||
// style: "build",
|
||||
// dir: projectRoot + "/pages",
|
||||
prefix: "_pages",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
app.serve({
|
||||
port: 3000,
|
||||
});
|
||||
}
|
||||
185
docs/rfcs/bun-build-config.ts
Normal file
185
docs/rfcs/bun-build-config.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Bundler API
|
||||
*
|
||||
* This is a proposal for the JavaScript API for Bun's native bundler.
|
||||
*/
|
||||
|
||||
import { FileBlob } from "bun";
|
||||
import { Log } from "./bun-build-logs";
|
||||
type BunPlugin = Parameters<(typeof Bun)["plugin"]>[0];
|
||||
export type JavaScriptLoader = "jsx" | "js" | "ts" | "tsx";
|
||||
export type MacroMap = Record<string, Record<string, string>>;
|
||||
export type Target = "bun" | "browser" | "node" | "neutral";
|
||||
export type ModuleFormat = "iife" | "cjs" | "esm";
|
||||
export type JsxTransform = "transform" | "preserve" | "automatic";
|
||||
export type Loader =
|
||||
| "base64"
|
||||
| "binary"
|
||||
| "copy"
|
||||
| "css"
|
||||
| "dataurl"
|
||||
| "default"
|
||||
| "empty"
|
||||
| "file"
|
||||
| "js"
|
||||
| "json"
|
||||
| "jsx"
|
||||
| "text"
|
||||
| "ts"
|
||||
| "tsx";
|
||||
export type ImportKind =
|
||||
| "entry-point"
|
||||
| "import-statement"
|
||||
| "require-call"
|
||||
| "dynamic-import"
|
||||
| "require-resolve"
|
||||
| "import-rule"
|
||||
| "url-token";
|
||||
export type LogLevel = "verbose" | "debug" | "info" | "warning" | "error" | "silent";
|
||||
export type Charset = "ascii" | "utf8";
|
||||
|
||||
type BundlerError = {
|
||||
file: string;
|
||||
error: Error;
|
||||
};
|
||||
|
||||
export interface BuildConfig extends BundlerConfig {
|
||||
entrypoints: string[];
|
||||
outdir: string;
|
||||
root?: string; // equiv. outbase
|
||||
watch?: boolean;
|
||||
}
|
||||
|
||||
export interface BundlerConfig {
|
||||
label?: string; // default "default"
|
||||
bundle?: boolean; // default true
|
||||
splitting?: boolean;
|
||||
plugins?: BunPlugin[];
|
||||
|
||||
// dropped: preserveSymlinks. defer to tsconfig for this.
|
||||
|
||||
// whether to parse manifest after build
|
||||
manifest?: boolean;
|
||||
|
||||
naming?:
|
||||
| string
|
||||
| {
|
||||
/** Documentation: https://esbuild.github.io/api/#entry-names */
|
||||
entry?: string;
|
||||
/** Documentation: https://esbuild.github.io/api/#chunk-names */
|
||||
chunk?: string;
|
||||
/** Documentation: https://esbuild.github.io/api/#asset-names */
|
||||
asset?: string;
|
||||
extensions?: { [ext: string]: string };
|
||||
};
|
||||
|
||||
/** Documentation: https://esbuild.github.io/api/#external */
|
||||
external?: Array<string | RegExp>;
|
||||
|
||||
// set environment variables
|
||||
env?: Record<string, string>;
|
||||
|
||||
// transform options only apply to entrypoints
|
||||
imports?: {
|
||||
rename?: Record<string, string>;
|
||||
};
|
||||
exports?: {
|
||||
pick?: string[];
|
||||
omit?: string[];
|
||||
rename?: Record<string, string>;
|
||||
};
|
||||
|
||||
// export conditions in priority order
|
||||
conditions?: string[];
|
||||
|
||||
origin?: string; // e.g. http://mydomain.com
|
||||
|
||||
// in Bun.Transpiler this only accepts a Loader string
|
||||
// change: set an ext->loader map
|
||||
loader?: { [k in string]: Loader };
|
||||
|
||||
// rename `platform` to `target`
|
||||
target?: Target;
|
||||
|
||||
// path to a tsconfig.json file
|
||||
// or a parsed object
|
||||
// passing in a stringified json is weird
|
||||
tsconfig?: string | object;
|
||||
|
||||
// from Bun.Transpiler API
|
||||
macro?: MacroMap;
|
||||
|
||||
sourcemap?:
|
||||
| "none"
|
||||
| "inline"
|
||||
| "external"
|
||||
| {
|
||||
root?: string;
|
||||
inline?: boolean;
|
||||
external?: boolean;
|
||||
|
||||
// probably unnecessary
|
||||
content?: boolean;
|
||||
};
|
||||
|
||||
module?: ModuleFormat;
|
||||
|
||||
// removed: logging, mangleProps, reserveProps, mangleQuoted, mangleCache
|
||||
|
||||
/** Documentation: https://esbuild.github.io/api/#minify */
|
||||
minify?:
|
||||
| boolean
|
||||
| {
|
||||
whitespace?: boolean;
|
||||
identifiers?: boolean;
|
||||
syntax?: boolean;
|
||||
};
|
||||
|
||||
treeshaking?: boolean;
|
||||
|
||||
jsx?:
|
||||
| JsxTransform
|
||||
| {
|
||||
transform?: JsxTransform;
|
||||
factory?: string;
|
||||
fragment?: string;
|
||||
importSource?: string;
|
||||
development?: boolean;
|
||||
sideEffects?: boolean;
|
||||
inline?: boolean;
|
||||
optimizeReact?: boolean;
|
||||
autoImport?: boolean;
|
||||
};
|
||||
|
||||
charset?: Charset;
|
||||
}
|
||||
|
||||
// copied from esbuild
|
||||
export type BuildManifest = {
|
||||
inputs: {
|
||||
[path: string]: {
|
||||
output: string; // path to corresponding entry bundle
|
||||
imports: {
|
||||
path: string;
|
||||
kind: ImportKind;
|
||||
external?: boolean;
|
||||
asset?: boolean; // whether the import defaulted to "file" loader
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
// less important than `inputs`
|
||||
outputs: {
|
||||
[path: string]: {
|
||||
type: "chunk" | "entry-point" | "asset";
|
||||
inputs: { path: string }[];
|
||||
imports: {
|
||||
path: string;
|
||||
kind: ImportKind;
|
||||
external?: boolean;
|
||||
}[];
|
||||
exports: string[];
|
||||
entrypoint?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
40
docs/rfcs/bun-build-logs.ts
Normal file
40
docs/rfcs/bun-build-logs.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export const enum MessageLevel {
|
||||
err = 1,
|
||||
warn = 2,
|
||||
note = 3,
|
||||
info = 4,
|
||||
debug = 5,
|
||||
}
|
||||
|
||||
export interface Location {
|
||||
file: string;
|
||||
namespace: string;
|
||||
line: number;
|
||||
column: number;
|
||||
line_text: string;
|
||||
suggestion: string;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export interface MessageData {
|
||||
text?: string;
|
||||
location?: Location;
|
||||
}
|
||||
|
||||
export interface MessageMeta {
|
||||
resolve?: string;
|
||||
build?: boolean;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
level: MessageLevel;
|
||||
data: MessageData;
|
||||
notes: MessageData[];
|
||||
on: MessageMeta;
|
||||
}
|
||||
|
||||
export interface Log {
|
||||
warnings: number;
|
||||
errors: number;
|
||||
msgs: Message[];
|
||||
}
|
||||
51
docs/rfcs/bun-build.ts
Normal file
51
docs/rfcs/bun-build.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { FileBlob } from "bun";
|
||||
import { BuildConfig, BuildManifest, BundlerConfig } from "./bun-build-config";
|
||||
import { Log } from "./bun-build-logs";
|
||||
|
||||
namespace Bun {
|
||||
export declare function build(config: BuildConfig): BuildResult<Blob>;
|
||||
}
|
||||
|
||||
export type BuildResult<T> = {
|
||||
// T will usually be a FileBlob
|
||||
// or a Blob (for in-memory builds)
|
||||
outputs: Map<string, FileBlob | Blob>;
|
||||
// only exists if `manifest` is true
|
||||
manifest?: BuildManifest;
|
||||
log: Log;
|
||||
};
|
||||
|
||||
// simple build, writes to disk
|
||||
{
|
||||
Bun.build({
|
||||
target: "bun",
|
||||
entrypoints: ["index.js"],
|
||||
naming: "[name]-[hash].[ext]",
|
||||
outdir: "./build",
|
||||
});
|
||||
}
|
||||
|
||||
// RSC
|
||||
{
|
||||
Bun.build({
|
||||
target: "bun",
|
||||
entrypoints: ["index.js"],
|
||||
naming: "[name]-[hash].[ext]",
|
||||
outdir: "./build",
|
||||
plugins: [
|
||||
//@ts-ignore
|
||||
BunPluginRSC({
|
||||
client: {
|
||||
entrypoints: ["client-entry.ts"],
|
||||
outdir: "./build/client",
|
||||
/* ... */
|
||||
},
|
||||
ssr: {
|
||||
entrypoints: ["ssr-entry.ts"],
|
||||
outdir: "./build/ssr",
|
||||
/* ... */
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user