Skip to content
25 changes: 23 additions & 2 deletions docs/runtime/transpiler.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,31 @@ interface TranspilerOptions {
macro?: MacroMap,

// Specify a set of exports to eliminate
// Or rename certain exports
// Or replace certain exports
exports?: {
eliminate?: string[];
replace?: Record<string, string>;
// A string, number, boolean, null or undefined becomes the value of the export
// [name, value] replaces the export with an export called name, with that value
// Bun leaves these exports as they are:
// - export function and export class, when deadCodeElimination is false
// - a name that a destructuring pattern declares
// - export default class, when the value for default is not a pair
// - export { local as name } with no from, when the key is name
// For that list the key is local, and only a pair works: a plain value declares local a second time
// For a pair or a map that you keep in a variable, write as const
// Write each key as an identifier or default, and each name as an identifier
// The constructor throws a TypeError for a key or a name such as a-b
// Give each pair a name that the top level of the file does not declare
// transformSync() throws for a name that is taken, or when two replaced exports of one file take the same name
replace?: Record<
string,
| string
| number
| boolean
| null
| undefined
| readonly [name: string, value: string | number | boolean | null | undefined]
>;
},

// Whether to remove unused imports from transpiled file
Expand Down
65 changes: 64 additions & 1 deletion packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3011,7 +3011,70 @@ declare module "bun" {
allowBunRuntime?: boolean;
exports?: {
eliminate?: string[];
replace?: Record<string, string>;
/**
* Replace exported declarations of the file. Each key is the name of an
* exported `const`, `let`, `var`, `function` or `class` declaration, or
* `default` for `export default`. Its value is the replacement.
*
* - A string, number, boolean, `null` or `undefined` becomes the value
* of the declaration. The declaration keeps its name.
Comment thread
robobun marked this conversation as resolved.
Resolved
* - `[name, value]` replaces the declaration with an export called
* `name`. The export has the value `value`, which is a string,
* number, boolean, `null` or `undefined`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Comment thread
robobun marked this conversation as resolved.
Resolved
*
* Bun leaves these exports as they are:
* - `export function` and `export class`, when `deadCodeElimination`
* is `false`. The default is `true`.
* - A name that a destructuring pattern declares, such as `a` in
* `export const { a } = value`.
* - `export default class`, when the value for `default` is not a pair.
* - `export { local as name }` with no `from`, when the key is `name`.
*
* For that list the key is `local`, and only a pair works. A plain
* value adds a second declaration of `local`.
*
* For a pair or a map that you keep in a variable, write `as const`:
* `const pair = ["__N_SSG", true] as const`. Without a type, TypeScript
* infers an array for the pair and reports an error.
*
* Write each key as an identifier or `default`, and each `name` as an
* identifier. The constructor throws a `TypeError` for a key or a
* `name` such as `a-b`.
*
* Give each pair a `name` that the top level of the file does not
* declare. `transformSync()` throws when the top level declares a
* variable, function or class with that `name`, or when two replaced
* exports of one file take the same `name`.
*
* @example
* ```ts
* const transpiler = new Bun.Transpiler({
* loader: "ts",
* exports: {
* replace: {
* revalidate: 60,
* getStaticProps: ["__N_SSG", true],
* },
* },
* });
*
* transpiler.transformSync(`
* export const revalidate = readConfig().revalidate;
* export const getStaticProps = async () => ({ props: await load() });
* `);
* // export const revalidate = 60;
* // export const __N_SSG = true;
* ```
*/
replace?: Record<
string,
| string
| number
| boolean
| null
| undefined
| readonly [name: string, value: string | number | boolean | null | undefined]
Comment thread
robobun marked this conversation as resolved.
Resolved
>;
};
treeShaking?: boolean;
trimUnusedImports?: boolean;
Expand Down
150 changes: 150 additions & 0 deletions test/integration/bun-types/fixture/transpiler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { expectType } from "./utilities";

type ExportsReplace = NonNullable<NonNullable<Bun.TranspilerOptions["exports"]>["replace"]>;

expectType<ExportsReplace[string]>().is<
| string
| number
| boolean
| null
| undefined
| readonly [name: string, value: string | number | boolean | null | undefined]
>();

// The example in the JSDoc of `exports.replace`.
{
const transpiler = new Bun.Transpiler({
loader: "ts",
exports: {
replace: {
revalidate: 60,
getStaticProps: ["__N_SSG", true],
},
},
});

expectType(
transpiler.transformSync(`
export const revalidate = readConfig().revalidate;
export const getStaticProps = async () => ({ props: await load() });
`),
).is<string>();
}

// A plain value becomes the value of the export.
new Bun.Transpiler({
exports: {
replace: {
aString: "bar",
aNumber: 60,
aBoolean: true,
aNull: null,
anUndefined: undefined,
},
},
});

// `[name, value]` replaces the export with an export called `name`.
new Bun.Transpiler({
exports: {
replace: {
withString: ["__NAME", "bar"],
withNumber: ["__NAME", 60],
withBoolean: ["__N_SSG", true],
withNull: ["__NAME", null],
withUndefined: ["__NAME", undefined],
},
},
});

// The runtime only reads the pair, so a readonly tuple is accepted.
// Two keys can take one name, when each file exports only one of the two keys.
{
const entry = ["__N_SSG", true] as const;
new Bun.Transpiler({ exports: { replace: { getStaticProps: entry, getStaticPaths: entry } } });
}

// A map that is built before the constructor call.
{
const replace: ExportsReplace = {};
for (const name of ["getStaticProps", "getStaticPaths"]) {
replace[name] = ["__N_SSG", true];
}
replace.revalidate = 60;
new Bun.Transpiler({ exports: { eliminate: ["loader"], replace } });
}

// Maps with a type of their own, such as the type of the declaration before this one.
{
const strings: Record<string, string> = { version: "1.0.0" };
new Bun.Transpiler({ exports: { replace: strings } });

const pairs: Record<string, number | [string, boolean]> = { revalidate: 60, getStaticProps: ["__N_SSG", true] };
new Bun.Transpiler({ exports: { replace: pairs } });
}

// A pair or a map in a variable. The call gives no type to a literal that is written before it.
{
const constMap = { revalidate: 60, getStaticProps: ["__N_SSG", true] } as const;
new Bun.Transpiler({ exports: { replace: constMap } });

const mapWithConstPair = { revalidate: 60, getStaticProps: ["__N_SSG", true] as const };
new Bun.Transpiler({ exports: { replace: mapWithConstPair } });

const checkedMap = { revalidate: 60, getStaticProps: ["__N_SSG", true] } satisfies ExportsReplace;
new Bun.Transpiler({ exports: { replace: checkedMap } });

const typedPair: [string, boolean] = ["__N_SSG", true];
new Bun.Transpiler({ exports: { replace: { getStaticProps: typedPair } } });

const options: Bun.TranspilerOptions = { exports: { replace: { getStaticProps: ["__N_SSG", true] } } };
new Bun.Transpiler(options);

const inferredPair = ["__N_SSG", true];
// @ts-expect-error - without a type, TypeScript infers (string | boolean)[] for the pair
new Bun.Transpiler({ exports: { replace: { getStaticProps: inferredPair } } });

const inferredMap = { revalidate: 60, getStaticProps: ["__N_SSG", true] };
// @ts-expect-error - without a type, TypeScript infers (string | boolean)[] for the pair
new Bun.Transpiler({ exports: { replace: inferredMap } });
}

// The `default` key takes a value or a pair.
{
new Bun.Transpiler({ exports: { replace: { default: 60 } } });
new Bun.Transpiler({ exports: { replace: { default: "bar", revalidate: 60 } } });
new Bun.Transpiler({ exports: { replace: { default: ["__N_SSG", true] } } });

const entry = ["__N_SSG", true] as const;
new Bun.Transpiler({ exports: { replace: { default: entry } } });

const replace: ExportsReplace = {};
replace.default = 60;
replace.default = ["__N_SSG", true];
}

// The constructor throws for each of these values.
new Bun.Transpiler({
exports: {
replace: {
// @ts-expect-error - bigint is not a replacement value
aBigInt: 1n,
// @ts-expect-error - symbol is not a replacement value
aSymbol: Symbol("s"),
// @ts-expect-error - function is not a replacement value
aFunction: () => 1,
// @ts-expect-error - object is not a replacement value
anObject: { value: 1 },
// @ts-expect-error - the pair needs a value
nameOnly: ["__NAME"],
// @ts-expect-error - the pair has two elements
threeElements: ["__NAME", true, 1],
// @ts-expect-error - the value of the pair is one of the plain values
objectInPair: ["__NAME", { value: 1 }],
// @ts-expect-error - the value of the pair cannot be another pair
pairInPair: ["__NAME", ["__OTHER", 1]],
// @ts-expect-error - the name of the pair is a string
numberAsName: [1, true],
},
},
});
Loading