Skip to content

Commit 07dc1ae

Browse files
authored
.defer(), .onStart(), and some small CSS changes (#15041)
1 parent 8f5eab3 commit 07dc1ae

18 files changed

Lines changed: 2682 additions & 1014 deletions

File tree

docs/runtime/plugins.md

Lines changed: 247 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ require("my-object-virtual-module"); // { baz: "quix" }
302302
await import("my-object-virtual-module"); // { baz: "quix" }
303303
```
304304

305-
## Reading the config
305+
## Reading or modifying the config
306306

307307
Plugins can read and write to the [build config](https://bun.sh/docs/bundler#api) with `build.config`.
308308

@@ -327,7 +327,43 @@ Bun.build({
327327
});
328328
```
329329

330-
## Reference
330+
{% callout %}
331+
332+
**NOTE**: Plugin lifcycle callbacks (`onStart()`, `onResolve()`, etc.) do not have the ability to modify the `build.config` object in the `setup()` function. If you want to mutate `build.config`, you must do so directly in the `setup()` function:
333+
334+
```ts
335+
Bun.build({
336+
entrypoints: ["./app.ts"],
337+
outdir: "./dist",
338+
sourcemap: "external",
339+
plugins: [
340+
{
341+
name: "demo",
342+
setup(build) {
343+
// ✅ good! modifying it directly in the setup() function
344+
build.config.minify = true;
345+
346+
build.onStart(() => {
347+
// 🚫 uh-oh! this won't work!
348+
build.config.minify = false;
349+
});
350+
},
351+
},
352+
],
353+
});
354+
```
355+
356+
{% /callout %}
357+
358+
## Lifecycle callbacks
359+
360+
Plugins can register callbacks to be run at various points in the lifecycle of a bundle:
361+
362+
- [`onStart()`](#onstart): Run once the bundler has started a bundle
363+
- [`onResolve()`](#onresolve): Run before a module is resolved
364+
- [`onLoad()`](#onload): Run before a module is loaded.
365+
366+
A rough overview of the types (please refer to Bun's `bun.d.ts` for the full type definitions):
331367

332368
```ts
333369
namespace Bun {
@@ -338,6 +374,7 @@ namespace Bun {
338374
}
339375

340376
type PluginBuilder = {
377+
onStart(callback: () => void): void;
341378
onResolve: (
342379
args: { filter: RegExp; namespace?: string },
343380
callback: (args: { path: string; importer: string }) => {
@@ -356,7 +393,213 @@ type PluginBuilder = {
356393
config: BuildConfig;
357394
};
358395

359-
type Loader = "js" | "jsx" | "ts" | "tsx" | "json" | "toml" | "object";
396+
type Loader = "js" | "jsx" | "ts" | "tsx" | "css" | "json" | "toml" | "object";
397+
```
398+
399+
### Namespaces
400+
401+
`onLoad` and `onResolve` accept an optional `namespace` string. What is a namespaace?
402+
403+
Every module has a namespace. Namespaces are used to prefix the import in transpiled code; for instance, a loader with a `filter: /\.yaml$/` and `namespace: "yaml:"` will transform an import from `./myfile.yaml` into `yaml:./myfile.yaml`.
404+
405+
The default namespace is `"file"` and it is not necessary to specify it, for instance: `import myModule frmo "./my-module.ts"` is the same as `import myModule from "file:./my-module.ts"`.
406+
407+
Other common namespaces are:
408+
409+
- `"bun"`: for Bun-specific modules (e.g. `"bun:test"`, `"bun:sqlite"`)
410+
- `"node"`: for Node.js modules (e.g. `"node:fs"`, `"node:path"`)
411+
412+
### `onStart`
413+
414+
```ts
415+
onStart(callback: () => void): Promise<void> | void;
416+
```
417+
418+
Registers a callback to be run when the bundler starts a new bundle.
419+
420+
```ts
421+
import { plugin } from "bun";
422+
423+
plugin({
424+
name: "onStart example",
425+
426+
setup(build) {
427+
build.onStart(() => {
428+
console.log("Bundle started!");
429+
});
430+
},
431+
});
432+
```
433+
434+
The callback can return a `Promise`. After the bundle process has initialized, the bundler waits until all `onStart()` callbacks have completed before continuing.
435+
436+
For example:
437+
438+
```ts
439+
const result = await Bun.build({
440+
entrypoints: ["./app.ts"],
441+
outdir: "./dist",
442+
sourcemap: "external",
443+
plugins: [
444+
{
445+
name: "Sleep for 10 seconds",
446+
setup(build) {
447+
build.onStart(async () => {
448+
await Bunlog.sleep(10_000);
449+
});
450+
},
451+
},
452+
{
453+
name: "Log bundle time to a file",
454+
setup(build) {
455+
build.onStart(async () => {
456+
const now = Date.now();
457+
await Bun.$`echo ${now} > bundle-time.txt`;
458+
});
459+
},
460+
},
461+
],
462+
});
463+
```
464+
465+
In the above example, Bun will wait until the first `onStart()` (sleeping for 10 seconds) has completed, _as well as_ the second `onStart()` (writing the bundle time to a file).
466+
467+
Note that `onStart()` callbacks (like every other lifecycle callback) do not have the ability to modify the `build.config` object. If you want to mutate `build.config`, you must do so directly in the `setup()` function.
468+
469+
### `onResolve`
470+
471+
```ts
472+
onResolve(
473+
args: { filter: RegExp; namespace?: string },
474+
callback: (args: { path: string; importer: string }) => {
475+
path: string;
476+
namespace?: string;
477+
} | void,
478+
): void;
479+
```
480+
481+
To bundle your project, Bun walks down the dependency tree of all modules in your project. For each imported module, Bun actually has to find and read that module. The "finding" part is known as "resolving" a module.
482+
483+
The `onResolve()` plugin lifecycle callback allows you to configure how a module is resolved.
484+
485+
The first argument to `onResolve()` is an object with a `filter` and [`namespace`](#what-is-a-namespace) property. The filter is a regular expression which is run on the import string. Effectively, these allow you to filter which modules your custom resolution logic will apply to.
486+
487+
The second argument to `onResolve()` is a callback which is run for each module import Bun finds that matches the `filter` and `namespace` defined in the first argument.
488+
489+
The callback receives as input the _path_ to the matching module. The callback can return a _new path_ for the module. Bun will read the contents of the _new path_ and parse it as a module.
490+
491+
For example, redirecting all imports to `images/` to `./public/images/`:
492+
493+
```ts
494+
import { plugin } from "bun";
495+
496+
plugin({
497+
name: "onResolve example",
498+
setup(build) {
499+
build.onResolve({ filter: /.*/, namespace: "file" }, args => {
500+
if (args.path.startsWith("images/")) {
501+
return {
502+
path: args.path.replace("images/", "./public/images/"),
503+
};
504+
}
505+
});
506+
},
507+
});
508+
```
509+
510+
### `onLoad`
511+
512+
```ts
513+
onLoad(
514+
args: { filter: RegExp; namespace?: string },
515+
callback: (args: { path: string, importer: string, namespace: string, kind: ImportKind }) => {
516+
loader?: Loader;
517+
contents?: string;
518+
exports?: Record<string, any>;
519+
},
520+
): void;
521+
```
522+
523+
After Bun's bundler has resolved a module, it needs to read the contents of the module and parse it.
524+
525+
The `onLoad()` plugin lifecycle callback allows you to modify the _contents_ of a module before it is read and parsed by Bun.
526+
527+
Like `onResolve()`, the first argument to `onLoad()` allows you to filter which modules this invocation of `onLoad()` will apply to.
528+
529+
The second argument to `onLoad()` is a callback which is run for each matching module _before_ Bun loads the contents of the module into memory.
530+
531+
This callback receives as input the _path_ to the matching module, the _importer_ of the module (the module that imported the module), the _namespace_ of the module, and the _kind_ of the module.
532+
533+
The callback can return a new `contents` string for the module as well as a new `loader`.
534+
535+
For example:
536+
537+
```ts
538+
import { plugin } from "bun";
539+
540+
plugin({
541+
name: "env plugin",
542+
setup(build) {
543+
build.onLoad({ filter: /env/, namespace: "file" }, args => {
544+
return {
545+
contents: `export default ${JSON.stringify(process.env)}`,
546+
loader: "js",
547+
};
548+
});
549+
},
550+
});
551+
```
552+
553+
This plugin will transform all imports of the form `import env from "env"` into a JavaScript module that exports the current environment variables.
554+
555+
#### `.defer()`
556+
557+
One of the arguments passed to the `onLoad` callback is a `defer` function. This function returns a `Promise` that is resolved when all _other_ modules have been loaded.
558+
559+
This allows you to delay execution of the `onLoad` callback until all other modules have been loaded.
560+
561+
This is useful for returning contens of a module that depends on other modules.
562+
563+
##### Example: tracking and reporting unused exports
564+
565+
```ts
566+
import { plugin } from "bun";
567+
568+
plugin({
569+
name: "track imports",
570+
setup(build) {
571+
const transpiler = new Bun.Transpiler();
572+
573+
let trackedImports: Record<string, number> = {};
574+
575+
// Each module that goes through this onLoad callback
576+
// will record its imports in `trackedImports`
577+
build.onLoad({ filter: /\.ts/ }, async ({ path }) => {
578+
const contents = await Bun.file(path).arrayBuffer();
579+
580+
const imports = transpiler.scanImports(contents);
581+
582+
for (const i of imports) {
583+
trackedImports[i.path] = (trackedImports[i.path] || 0) + 1;
584+
}
585+
586+
return undefined;
587+
});
588+
589+
build.onLoad({ filter: /stats\.json/ }, async ({ defer }) => {
590+
// Wait for all files to be loaded, ensuring
591+
// that every file goes through the above `onLoad()` function
592+
// and their imports tracked
593+
await defer();
594+
595+
// Emit JSON containing the stats of each import
596+
return {
597+
contents: `export default ${JSON.stringify(trackedImports)}`,
598+
loader: "json",
599+
};
600+
});
601+
},
602+
});
360603
```
361604

362-
The `onLoad` method optionally accepts a `namespace` in addition to the `filter` regex. This namespace will be be used to prefix the import in transpiled code; for instance, a loader with a `filter: /\.yaml$/` and `namespace: "yaml:"` will transform an import from `./myfile.yaml` into `yaml:./myfile.yaml`.
605+
Note that the `.defer()` function currently has the limitation that it can only be called once per `onLoad` callback.

packages/bun-types/bun.d.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3785,7 +3785,7 @@ declare module "bun" {
37853785
| "browser";
37863786

37873787
/** https://bun.sh/docs/bundler/loaders */
3788-
type Loader = "js" | "jsx" | "ts" | "tsx" | "json" | "toml" | "file" | "napi" | "wasm" | "text";
3788+
type Loader = "js" | "jsx" | "ts" | "tsx" | "json" | "toml" | "file" | "napi" | "wasm" | "text" | "css";
37893789

37903790
interface PluginConstraints {
37913791
/**
@@ -3873,10 +3873,18 @@ declare module "bun" {
38733873
* The default loader for this file extension
38743874
*/
38753875
loader: Loader;
3876+
3877+
/**
3878+
* Defer the execution of this callback until all other modules have been parsed.
3879+
*
3880+
* @returns Promise which will be resolved when all modules have been parsed
3881+
*/
3882+
defer: () => Promise<void>;
38763883
}
38773884

38783885
type OnLoadResult = OnLoadResultSourceCode | OnLoadResultObject | undefined;
38793886
type OnLoadCallback = (args: OnLoadArgs) => OnLoadResult | Promise<OnLoadResult>;
3887+
type OnStartCallback = () => void | Promise<void>;
38803888

38813889
interface OnResolveArgs {
38823890
/**
@@ -3953,6 +3961,20 @@ declare module "bun" {
39533961
* ```
39543962
*/
39553963
onResolve(constraints: PluginConstraints, callback: OnResolveCallback): void;
3964+
/**
3965+
* Register a callback which will be invoked when bundling starts.
3966+
* @example
3967+
* ```ts
3968+
* Bun.plugin({
3969+
* setup(builder) {
3970+
* builder.onStart(() => {
3971+
* console.log("bundle just started!!")
3972+
* });
3973+
* },
3974+
* });
3975+
* ```
3976+
*/
3977+
onStart(callback: OnStartCallback): void;
39563978
/**
39573979
* The config object passed to `Bun.build` as is. Can be mutated.
39583980
*/

0 commit comments

Comments
 (0)