Repository: rolldown/tsdown
Pull request #152: feat(cli): support compile-time env
Author: login: fu050409
Created: 2025-04-25T07:49:40Z | Merged: 2025-04-25T15:31:25Z

### Description

Support compile-time env in command-line tool

### Linked Issues

Resolved #138 

### Additional context

<!-- e.g. is there anything you'd like reviewers to focus on? -->


Linked issue #138: Add support for compile-time environment variables
`tsup` supports [compile-time environment variables](https://tsup.egoist.dev/#compile-time-environment-variables), which allow you to specify values for environment variables using the `--env` flag:

```
tsup --env.NODE_ENV production
```

I think you can achieve a similar result by using `inputOptions` and `NODE_ENV=production tsdown`:

```ts
export default defineConfig(() => {
  return {
    inputOptions: {
      define: {
        'process.env.NODE_ENV': process.env.NODE_ENV ?? undefined,
      },
    }
  };
});
```

However, it would be nice to have the same CLI flag to achieve this without modifying the config.

Diff:
diff --git a/docs/reference/cli.md b/docs/reference/cli.md
index 25a25235e..5f7cfd6e4 100644
--- a/docs/reference/cli.md
+++ b/docs/reference/cli.md
@@ -131,6 +131,16 @@ See also [Extending Vite or Vitest Config](../guide/config-file.md#extending-vit
 
 Enable or disable the generation of a build report. By default, the report is enabled and outputs the list of build artifacts along with their sizes to the console. This provides a quick overview of the build results, helping you analyze the output and identify potential optimizations. Disabling the report can be useful in scenarios where minimal console output is desired.
 
+## `--env.* <value>`
+
+Define compile-time environment variables, for example:
+
+```bash
+tsdown --env.NODE_ENV=production
+```
+
+Note that environment variables defined with `--env.VAR_NAME` can only be accessed as `import.meta.env.VAR_NAME` or `process.env.VAR_NAME`.
+
 ## `--debug [feat]`
 
 Show debug logs.
diff --git a/docs/zh-CN/reference/cli.md b/docs/zh-CN/reference/cli.md
index 7ccbcab6d..e4836eee9 100644
--- a/docs/zh-CN/reference/cli.md
+++ b/docs/zh-CN/reference/cli.md
@@ -131,6 +131,16 @@ tsdown src/index.ts src/util.ts
 
 启用或禁用构建报告的生成。默认情况下，报告是启用的，并会将构建产物及其大小列表输出到控制台。这可以快速概览构建结果，帮助你分析输出并识别潜在的优化空间。在需要最小化控制台输出的场景下，可以禁用报告。
 
+## `--env.* <value>`
+
+定义编译时环境变量，例如：
+
+```bash
+tsdown --env.NODE_ENV=production
+```
+
+注意，使用 `--env.VAR_NAME` 设置的环境变量只能以 `import.meta.env.VAR_NAME` 或 `process.env.VAR_NAME` 访问。
+
 ## `--debug [feat]`
 
 显示调试日志。
diff --git a/src/cli.ts b/src/cli.ts
index 124131fb4..93df927c1 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -39,6 +39,7 @@ cli
   .option('-w, --watch [path]', 'Watch mode')
   .option('--from-vite [vitest]', 'Reuse config from Vite or Vitest')
   .option('--report', 'Size report', { default: true })
+  .option('--env.* <value>', 'Define compile-time env variables')
   .action(async (input: string[], flags: Options) => {
     setSilent(!!flags.silent)
     logger.info(
@@ -76,7 +77,7 @@ export async function runCLI(): Promise<void> {
     if (enabled) namespace += `,${enabled}`
 
     debug.enable(namespace)
-    debug('tsdown:debug')(`Debugging enabled`, namespace)
+    debug('tsdown:debug')('Debugging enabled', namespace)
   }
 
   try {
diff --git a/src/index.ts b/src/index.ts
index c13279e71..61166a103 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -194,6 +194,7 @@ async function getBuildOptions(
     tsconfig,
     cwd,
     report,
+    env,
   } = config
 
   const plugins: RolldownPluginOption = []
@@ -246,7 +247,15 @@ async function getBuildOptions(
       },
       treeshake,
       platform,
-      define,
+      define: {
+        ...define,
+        ...Object.keys(env).reduce((acc, key) => {
+          const value = JSON.stringify(env[key])
+          acc[`process.env.${key}`] = value
+          acc[`import.meta.env.${key}`] = value
+          return acc
+        }, Object.create(null)),
+      },
       plugins,
       inject: {
         ...(shims && !cjsDts && getShimsInject(format, platform)),
diff --git a/src/options.ts b/src/options.ts
index 39ad950b8..27db4baa7 100644
--- a/src/options.ts
+++ b/src/options.ts
@@ -147,6 +147,18 @@ export interface Options {
    */
   report?: boolean | ReportOptions
 
+  /**
+   * Compile-time env variables.
+   * @example
+   * ```ts
+   * {
+   *   "DEBUG": true,
+   *   "NODE_ENV": "production"
+   * }
+   * ```
+   */
+  env?: Record<string, any>
+
   hooks?:
     | Partial<TsdownHooks>
     | ((hooks: Hookable<TsdownHooks>) => Awaitable<void>)
@@ -222,6 +234,7 @@ export async function resolveOptions(options: Options): Promise<{
         tsconfig,
         report = true,
         target,
+        env = {},
       } = subOptions
 
       entry = await resolveEntry(entry, cwd)
@@ -306,6 +319,7 @@ export async function resolveOptions(options: Options): Promise<{
         alias,
         tsconfig,
         cwd,
+        env,
       }
 
       return config
diff --git a/tests/__snapshots__/env-flag.snap.md b/tests/__snapshots__/env-flag.snap.md
new file mode 100644
index 000000000..b4795eaaf
--- /dev/null
+++ b/tests/__snapshots__/env-flag.snap.md
@@ -0,0 +1,12 @@
+## index.js
+
+```js
+//#region index.ts
+const env = "production";
+const meta = "production";
+const custom = "tsdown";
+const debug = true;
+
+//#endregion
+export { custom, debug, env, meta };
+```
\ No newline at end of file
diff --git a/tests/index.test.ts b/tests/index.test.ts
index ef2b00cea..4406d6419 100644
--- a/tests/index.test.ts
+++ b/tests/index.test.ts
@@ -230,3 +230,24 @@ test('hooks', async (context) => {
   })
   expect(fn).toBeCalledTimes(3)
 })
+
+test('env flag', async (context) => {
+  const files = {
+    'index.ts': `export const env = process.env.NODE_ENV
+    export const meta = import.meta.env.NODE_ENV
+    export const custom = import.meta.env.CUSTOM
+    export const debug = import.meta.env.DEBUG
+    `,
+  }
+  const { snapshot } = await testBuild(context, files, {
+    env: {
+      NODE_ENV: 'production',
+      CUSTOM: 'tsdown',
+      DEBUG: true,
+    },
+  })
+  expect(snapshot).contains('const env = "production"')
+  expect(snapshot).contains('const meta = "production"')
+  expect(snapshot).contains('const custom = "tsdown"')
+  expect(snapshot).contains('const debug = true')
+})


Reviews:
user: login: sxzz
body: Thanks!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-25T08:07:47Z
user: login: fu050409
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T08:45:52Z
user: login: sxzz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T08:56:53Z
user: login: sxzz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T08:58:56Z
user: login: fu050409
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T09:01:40Z
user: login: sxzz
body: Thanks!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-25T15:31:12Z

Review comments:
diff_hunk: @@ -147,6 +147,16 @@ export interface Options {
    */
   report?: boolean | ReportOptions
 
+  /**
+   * Compile-time env variables.
+   * @example
+   * {
+   *   "DEBUG": "true",
path: src/options.ts
user: login: sxzz
body: ```suggestion
   *   "DEBUG": true,
```
created_at: 2025-04-25T08:07:30Z
author_association: MEMBER
diff_hunk: @@ -147,6 +147,16 @@ export interface Options {
    */
   report?: boolean | ReportOptions
 
+  /**
+   * Compile-time env variables.
+   * @example
+   * {
+   *   "DEBUG": "true",
path: src/options.ts
user: login: fu050409
body: @sxzz `env` is parsed by `cac` in the terminal, and if you use `--env.DEBUG=true`, the value here is actually a string `"true"` rather than a boolean. In Node.js, `process.env` is defined as extending `Dict<string>`, so I think it should look like a string here rather than a boolean. Wdyt?
created_at: 2025-04-25T08:45:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -147,6 +147,16 @@ export interface Options {
    */
   report?: boolean | ReportOptions
 
+  /**
+   * Compile-time env variables.
+   * @example
+   * {
+   *   "DEBUG": "true",
path: src/options.ts
user: login: sxzz
body: Here is for JavaScript API. How to have a boolean type `true` in your case? We should add the case into unit test
created_at: 2025-04-25T08:56:53Z
author_association: MEMBER
diff_hunk: @@ -147,6 +147,16 @@ export interface Options {
    */
   report?: boolean | ReportOptions
 
+  /**
+   * Compile-time env variables.
+   * @example
+   * {
+   *   "DEBUG": "true",
path: src/options.ts
user: login: sxzz
body: In cac, the value will be `true` by using `--env.X` without value
created_at: 2025-04-25T08:58:55Z
author_association: MEMBER
diff_hunk: @@ -147,6 +147,16 @@ export interface Options {
    */
   report?: boolean | ReportOptions
 
+  /**
+   * Compile-time env variables.
+   * @example
+   * {
+   *   "DEBUG": "true",
path: src/options.ts
user: login: fu050409
body: > In cac, the value will be `true` by using `--env.X` without value

Thanks for pointing out.

> Here is for JavaScript API. How to have a boolean type `true` in your case? We should add the case into unit test

I'll make the change.

created_at: 2025-04-25T09:01:40Z
author_association: CONTRIBUTOR

PR comments:
user: login: netlify[bot]
created_at: 2025-04-25T07:50:04Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *tsdown* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | f5e6b90a577fd910d03cd1b4a500aa275d057292 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/tsdown/deploys/680b523cc894ee00088fb492 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-152--tsdown.netlify.app](https://deploy-preview-152--tsdown.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTE1Mi0tdHNkb3duLm5ldGxpZnkuYXBwIn0.BzQ6Bz5WrFeN4zug4_WHbcss5-39jNTQJFwPhiry56M)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [tsdown Preview](https://deploy-preview-152--tsdown.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/tsdown/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: pkg-pr-new[bot]
created_at: 2025-04-25T10:18:57Z
body: 
[Open in StackBlitz](https://pkg.pr.new/template/1920e281-6b24-4fc1-bae8-a67a39aed7fe)


```
npm i https://pkg.pr.new/tsdown@152
```


_commit: <a href="https://github.com/rolldown/tsdown/runs/41149408904"><code>f5e6b90</code></a>_

author_association: NONE