From 33cf480f432438d16116d34eb42c015577edebcd Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:43:33 +0000 Subject: [PATCH 1/6] bun-types: declare the values exports.replace of Bun.Transpiler accepts The runtime accepts a string, number, boolean, null or undefined, and a [name, value] pair that replaces the export with an export called name. The declaration and the docs said Record. The default key takes a plain value only. A pair for the default export does not work with dead code elimination on. --- docs/runtime/transpiler.mdx | 17 ++- packages/bun-types/bun.d.ts | 44 +++++- .../bun-types/fixture/transpiler.ts | 128 ++++++++++++++++++ 3 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 test/integration/bun-types/fixture/transpiler.ts diff --git a/docs/runtime/transpiler.mdx b/docs/runtime/transpiler.mdx index 1c27654541db..6a622d571009 100644 --- a/docs/runtime/transpiler.mdx +++ b/docs/runtime/transpiler.mdx @@ -206,10 +206,23 @@ interface TranspilerOptions { macro?: MacroMap, // Specify a set of exports to eliminate - // Or rename certain exports + // Or replace certain exports exports?: { eliminate?: string[]; - replace?: Record; + // 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 + // The default key takes a value, and not a pair + replace?: Record< + string, + | string + | number + | boolean + | null + | undefined + | readonly [name: string, value: string | number | boolean | null | undefined] + > & { + default?: string | number | boolean | null | undefined; + }; }, // Whether to remove unused imports from transpiled file diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index e6e36301b614..da93af90ff70 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3011,7 +3011,49 @@ declare module "bun" { allowBunRuntime?: boolean; exports?: { eliminate?: string[]; - replace?: Record; + /** + * Replace exported declarations of the file. Each key is the name of an + * exported declaration, and its value is the replacement. + * + * - A string, number, boolean, `null` or `undefined` becomes the value + * of the declaration. The declaration keeps its name. + * - `[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`. + * + * The `default` key takes a value, and not a pair. + * + * @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] + > & { + default?: string | number | boolean | null | undefined; + }; }; treeShaking?: boolean; trimUnusedImports?: boolean; diff --git a/test/integration/bun-types/fixture/transpiler.ts b/test/integration/bun-types/fixture/transpiler.ts new file mode 100644 index 000000000000..0f2181fe44d1 --- /dev/null +++ b/test/integration/bun-types/fixture/transpiler.ts @@ -0,0 +1,128 @@ +import { expectType } from "./utilities"; + +type ExportsReplace = NonNullable["replace"]>; + +expectType().is< + | string + | number + | boolean + | null + | undefined + | readonly [name: string, value: string | number | boolean | null | undefined] +>(); +expectType().is(); + +// 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(); +} + +// 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. +{ + 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 = { version: "1.0.0" }; + new Bun.Transpiler({ exports: { replace: strings } }); + + const pairs: Record = { revalidate: 60, getStaticProps: ["__N_SSG", true] }; + new Bun.Transpiler({ exports: { replace: pairs } }); +} + +// The `default` key takes a value, and not a pair. +{ + new Bun.Transpiler({ exports: { replace: { default: 60 } } }); + new Bun.Transpiler({ exports: { replace: { default: "bar", revalidate: 60 } } }); + + // @ts-expect-error - the default key takes a value, and not a pair + new Bun.Transpiler({ exports: { replace: { default: ["__N_SSG", true] } } }); + + const entry = ["__N_SSG", true] as const; + // @ts-expect-error - the default key takes a value, and not a pair + new Bun.Transpiler({ exports: { replace: { default: entry } } }); + + const replace: ExportsReplace = {}; + replace.default = 60; + // @ts-expect-error - the default key takes a value, and not a pair + 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], + }, + }, +}); From ded39d0ca8a7be9498ea184884c4fba298998bb8 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 29 Sep 2026 06:00:48 +0000 Subject: [PATCH 2/6] bun-types: say how to write the keys and names of exports.replace The type of a key and of the name of a pair is string. The constructor throws a TypeError for one that is not written as an identifier, such as a-b. The JSDoc and the docs snippet now say so. --- docs/runtime/transpiler.mdx | 2 ++ packages/bun-types/bun.d.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/docs/runtime/transpiler.mdx b/docs/runtime/transpiler.mdx index 6a622d571009..b76cc8e8b41e 100644 --- a/docs/runtime/transpiler.mdx +++ b/docs/runtime/transpiler.mdx @@ -211,6 +211,8 @@ interface TranspilerOptions { eliminate?: 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 + // 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 // The default key takes a value, and not a pair replace?: Record< string, diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index da93af90ff70..36dfad00f46a 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3021,6 +3021,10 @@ declare module "bun" { * `name`. The export has the value `value`, which is a string, * number, boolean, `null` or `undefined`. * + * 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`. + * * The `default` key takes a value, and not a pair. * * @example From c92c2297c867e0099d0079d77a3ab016c87d8780 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 29 Sep 2026 06:46:03 +0000 Subject: [PATCH 3/6] bun-types: say how to keep a pair of exports.replace in a variable TypeScript infers an array for a pair that it meets with no type, and the declaration accepts a tuple. The JSDoc and the docs snippet name as const. The fixture has the spellings that compile and the two that do not. --- docs/runtime/transpiler.mdx | 1 + packages/bun-types/bun.d.ts | 4 +++ .../bun-types/fixture/transpiler.ts | 26 +++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/docs/runtime/transpiler.mdx b/docs/runtime/transpiler.mdx index b76cc8e8b41e..ad86e8d7e636 100644 --- a/docs/runtime/transpiler.mdx +++ b/docs/runtime/transpiler.mdx @@ -211,6 +211,7 @@ interface TranspilerOptions { eliminate?: 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 + // 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 // The default key takes a value, and not a pair diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 36dfad00f46a..7b8b7d66e632 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3021,6 +3021,10 @@ declare module "bun" { * `name`. The export has the value `value`, which is a string, * number, boolean, `null` or `undefined`. * + * 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`. diff --git a/test/integration/bun-types/fixture/transpiler.ts b/test/integration/bun-types/fixture/transpiler.ts index 0f2181fe44d1..8d3c2704cd83 100644 --- a/test/integration/bun-types/fixture/transpiler.ts +++ b/test/integration/bun-types/fixture/transpiler.ts @@ -83,6 +83,32 @@ new Bun.Transpiler({ 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, and not a pair. { new Bun.Transpiler({ exports: { replace: { default: 60 } } }); From d40aaa4417cbc4ef256cee80ec12eaf428fa907b Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 26 Oct 2026 06:33:52 +0000 Subject: [PATCH 4/6] bun-types: let the default key of exports.replace take a pair Since #46021 the runtime accepts `default: [name, value]` and prints `export var name = value;`. Remove the `default` clause from the type. Remove the sentence that forbids the pair from the JSDoc and from the docs page. The fixture now has a pair on the `default` key as accepted input. --- docs/runtime/transpiler.mdx | 5 +---- packages/bun-types/bun.d.ts | 6 +----- test/integration/bun-types/fixture/transpiler.ts | 7 +------ 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/docs/runtime/transpiler.mdx b/docs/runtime/transpiler.mdx index ad86e8d7e636..55ebffcdbc60 100644 --- a/docs/runtime/transpiler.mdx +++ b/docs/runtime/transpiler.mdx @@ -214,7 +214,6 @@ interface TranspilerOptions { // 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 - // The default key takes a value, and not a pair replace?: Record< string, | string @@ -223,9 +222,7 @@ interface TranspilerOptions { | null | undefined | readonly [name: string, value: string | number | boolean | null | undefined] - > & { - default?: string | number | boolean | null | undefined; - }; + >; }, // Whether to remove unused imports from transpiled file diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index c688be9c6e47..f1c1dd2c0808 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3029,8 +3029,6 @@ declare module "bun" { * identifier. The constructor throws a `TypeError` for a key or a * `name` such as `a-b`. * - * The `default` key takes a value, and not a pair. - * * @example * ```ts * const transpiler = new Bun.Transpiler({ @@ -3059,9 +3057,7 @@ declare module "bun" { | null | undefined | readonly [name: string, value: string | number | boolean | null | undefined] - > & { - default?: string | number | boolean | null | undefined; - }; + >; }; treeShaking?: boolean; trimUnusedImports?: boolean; diff --git a/test/integration/bun-types/fixture/transpiler.ts b/test/integration/bun-types/fixture/transpiler.ts index 8d3c2704cd83..ca3b9c567426 100644 --- a/test/integration/bun-types/fixture/transpiler.ts +++ b/test/integration/bun-types/fixture/transpiler.ts @@ -10,7 +10,6 @@ expectType().is< | undefined | readonly [name: string, value: string | number | boolean | null | undefined] >(); -expectType().is(); // The example in the JSDoc of `exports.replace`. { @@ -109,21 +108,17 @@ new Bun.Transpiler({ new Bun.Transpiler({ exports: { replace: inferredMap } }); } -// The `default` key takes a value, and not a pair. +// 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 } } }); - - // @ts-expect-error - the default key takes a value, and not a pair new Bun.Transpiler({ exports: { replace: { default: ["__N_SSG", true] } } }); const entry = ["__N_SSG", true] as const; - // @ts-expect-error - the default key takes a value, and not a pair new Bun.Transpiler({ exports: { replace: { default: entry } } }); const replace: ExportsReplace = {}; replace.default = 60; - // @ts-expect-error - the default key takes a value, and not a pair replace.default = ["__N_SSG", true]; } From 71310c5f8faa53527fa513344dbe05fd7c9d1277 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 26 Oct 2026 06:51:43 +0000 Subject: [PATCH 5/6] bun-types: say which exports a key of exports.replace names The JSDoc said that each key is the name of an exported declaration. The runtime matches a const, let, var, function or class declaration and export default. It leaves four exports as they are: export function and export class with deadCodeElimination false, a name from a destructuring pattern, export default class with a value that is not a pair, and the exported name of a local export list. --- docs/runtime/transpiler.mdx | 5 +++++ packages/bun-types/bun.d.ts | 12 +++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/runtime/transpiler.mdx b/docs/runtime/transpiler.mdx index 55ebffcdbc60..e81d24fbf802 100644 --- a/docs/runtime/transpiler.mdx +++ b/docs/runtime/transpiler.mdx @@ -211,6 +211,11 @@ interface TranspilerOptions { eliminate?: 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 (the key is local) // 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 diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index f1c1dd2c0808..c3d357eb49f7 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3013,7 +3013,8 @@ declare module "bun" { eliminate?: string[]; /** * Replace exported declarations of the file. Each key is the name of an - * exported declaration, and its value is the replacement. + * 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. @@ -3021,6 +3022,15 @@ declare module "bun" { * `name`. The export has the value `value`, which is a string, * number, boolean, `null` or `undefined`. * + * 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`. + * The key for that list is `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. From c6bc384ee6c6d2af9a7eadb65de96f60eab43108 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 26 Oct 2026 07:08:15 +0000 Subject: [PATCH 6/6] bun-types: state the limits of export lists and of pair names for exports.replace For an export list with no from, the key is the local name, and only a pair gives output that parses: a plain value adds a second declaration of the local name. transformSync() throws when the top level of the file declares the name of a pair, or when two replaced exports of one file take the same name. The JSDoc and the docs snippet say both. --- docs/runtime/transpiler.mdx | 5 ++++- packages/bun-types/bun.d.ts | 9 ++++++++- test/integration/bun-types/fixture/transpiler.ts | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/runtime/transpiler.mdx b/docs/runtime/transpiler.mdx index e81d24fbf802..a409c88a1677 100644 --- a/docs/runtime/transpiler.mdx +++ b/docs/runtime/transpiler.mdx @@ -215,10 +215,13 @@ interface TranspilerOptions { // - 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 (the key is local) + // - 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 diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index c3d357eb49f7..710547a2da95 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3029,7 +3029,9 @@ declare module "bun" { * `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`. - * The key for that list is `local`. + * + * 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 @@ -3039,6 +3041,11 @@ declare module "bun" { * 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({ diff --git a/test/integration/bun-types/fixture/transpiler.ts b/test/integration/bun-types/fixture/transpiler.ts index ca3b9c567426..ed9009755d32 100644 --- a/test/integration/bun-types/fixture/transpiler.ts +++ b/test/integration/bun-types/fixture/transpiler.ts @@ -58,6 +58,7 @@ new Bun.Transpiler({ }); // 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 } } });