diff --git a/docs/runtime/transpiler.mdx b/docs/runtime/transpiler.mdx index 1c27654541db..a409c88a1677 100644 --- a/docs/runtime/transpiler.mdx +++ b/docs/runtime/transpiler.mdx @@ -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; + // 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 diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 59aa530c8e58..710547a2da95 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3011,7 +3011,70 @@ declare module "bun" { allowBunRuntime?: boolean; exports?: { eliminate?: string[]; - replace?: Record; + /** + * 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. + * - `[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`. + * + * 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] + >; }; 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..ed9009755d32 --- /dev/null +++ b/test/integration/bun-types/fixture/transpiler.ts @@ -0,0 +1,150 @@ +import { expectType } from "./utilities"; + +type ExportsReplace = NonNullable["replace"]>; + +expectType().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(); +} + +// 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 = { 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 } }); +} + +// 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], + }, + }, +});