Compare commits

...

22 Commits

Author SHA1 Message Date
Colin McDonnell
84d5794477 Updates 2023-05-30 20:48:37 -07:00
Colin McDonnell
d3d22dc4f7 Updates 2023-05-30 17:54:23 -07:00
Colin McDonnell
21cb257651 Updates 2023-05-30 17:54:23 -07:00
Colin McDonnell
2fbe8ba36d UpdateS 2023-05-30 17:54:23 -07:00
Colin McDonnell
55dbe2a79a Typo 2023-05-30 17:54:23 -07:00
Colin McDonnell
ab345d110a Tweak sourcemap 2023-05-30 17:54:23 -07:00
Colin McDonnell
e0b01701df Updates 2023-05-30 17:54:23 -07:00
Colin McDonnell
5e86db7911 Updates 2023-05-30 17:54:23 -07:00
Colin McDonnell
c82345d0ff Update bun app examples 2023-05-30 17:54:23 -07:00
Colin McDonnell
c52ca33443 Import loader 2023-05-30 17:54:23 -07:00
Colin McDonnell
9abdf1b06c Routing-first app 2023-05-30 17:54:23 -07:00
Colin McDonnell
a9f0df0a06 Updates 2023-05-30 17:54:23 -07:00
Colin McDonnell
2319ab2232 Updates 2023-05-30 17:54:23 -07:00
Colin McDonnell
c6b9774c42 Updates 2023-05-30 17:54:19 -07:00
Colin McDonnell
d3dad085f3 engine -> app 2023-05-30 17:54:06 -07:00
Colin McDonnell
f49e30ddc3 Update static API 2023-05-30 17:54:06 -07:00
Colin McDonnell
3d5312ca4a Bun.Engine idea 2023-05-30 17:54:06 -07:00
Colin McDonnell
629317b642 Add multistage build proposal 2023-05-30 17:54:06 -07:00
Colin McDonnell
7c2bb689cd Update RFC 2023-05-30 17:54:06 -07:00
Colin McDonnell
26011ba009 Updates 2023-05-30 17:54:06 -07:00
Colin McDonnell
bbe2a638b6 WIP 2023-05-30 17:54:06 -07:00
Colin McDonnell
8ef998bfd9 First pass at bundler API 2023-05-30 17:54:01 -07:00
5 changed files with 576 additions and 2 deletions

View File

@@ -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
View 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,
});
}

View 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;
};
};
};

View 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
View 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",
/* ... */
},
}),
],
});
}