Skip to content

feat: support load env #518

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ test-results
.nx/
**/@mf-types
**/@mf-types/**
.env.local
.env.*.local
24 changes: 12 additions & 12 deletions packages/core/src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import type { RsbuildMode } from '@rsbuild/core';
import { type Command, program } from 'commander';
import { logger } from '../utils/logger';
import { build } from './build';
import { loadRslibConfig } from './init';
import { init } from './init';
import { inspect } from './inspect';
import { startMFDevServer } from './mf';
import { watchFilesForRestart } from './restart';

export type CommonOptions = {
root?: string;
config?: string;
envDir?: string;
envMode?: string;
lib?: string[];
};
Expand Down Expand Up @@ -37,7 +38,8 @@ const applyCommonOptions = (command: Command) => {
.option(
'--env-mode <mode>',
'specify the env mode to load the `.env.[mode]` file',
);
)
.option('--env-dir <dir>', 'specify the directory to load `.env` files');
};

const repeatableOption = (value: string, previous: string[]) => {
Expand All @@ -64,13 +66,12 @@ export function runCli(): void {
.action(async (options: BuildOptions) => {
try {
const cliBuild = async () => {
const { content: rslibConfig, filePath } =
await loadRslibConfig(options);
const { config, watchFiles } = await init(options);

await build(rslibConfig, options);
await build(config, options);

if (options.watch) {
watchFilesForRestart([filePath], async () => {
watchFilesForRestart(watchFiles, async () => {
await cliBuild();
});
}
Expand Down Expand Up @@ -100,8 +101,8 @@ export function runCli(): void {
.action(async (options: InspectOptions) => {
try {
// TODO: inspect should output Rslib's config
const { content: rslibConfig } = await loadRslibConfig(options);
await inspect(rslibConfig, {
const { config } = await init(options);
await inspect(config, {
lib: options.lib,
mode: options.mode,
output: options.output,
Expand All @@ -119,12 +120,11 @@ export function runCli(): void {
.action(async (options: CommonOptions) => {
try {
const cliMfDev = async () => {
const { content: rslibConfig, filePath } =
await loadRslibConfig(options);
const { config, watchFiles } = await init(options);
// TODO: support lib option in mf dev server
await startMFDevServer(rslibConfig);
await startMFDevServer(config);

watchFilesForRestart([filePath], async () => {
watchFilesForRestart(watchFiles, async () => {
await cliMfDev();
});
};
Expand Down
41 changes: 37 additions & 4 deletions packages/core/src/cli/init.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,51 @@
import path from 'node:path';
import { loadEnv } from '@rsbuild/core';
import { loadConfig } from '../config';
import type { RslibConfig } from '../types';
import { getAbsolutePath } from '../utils/helper';
import type { CommonOptions } from './commands';
import { onBeforeRestart } from './restart';

export async function loadRslibConfig(options: CommonOptions): Promise<{
content: RslibConfig;
filePath: string;
const getEnvDir = (cwd: string, envDir?: string) => {
if (envDir) {
return path.isAbsolute(envDir) ? envDir : path.resolve(cwd, envDir);
}
return cwd;
};

export async function init(options: CommonOptions): Promise<{
config: RslibConfig;
configFilePath: string;
watchFiles: string[];
}> {
const cwd = process.cwd();
const root = options.root ? getAbsolutePath(cwd, options.root) : cwd;
const envs = loadEnv({
cwd: getEnvDir(root, options.envDir),
mode: options.envMode,
});

onBeforeRestart(envs.cleanup);

return loadConfig({
const { content: config, filePath: configFilePath } = await loadConfig({
cwd: root,
path: options.config,
envMode: options.envMode,
});

config.source ||= {};
config.source.define = {
...envs.publicVars,
...config.source.define,
};

if (options.root) {
config.root = root;
}

return {
config,
configFilePath,
watchFiles: [configFilePath, ...envs.filePaths],
};
}
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions tests/integration/cli/env/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FOO=1
BAR=2
2 changes: 2 additions & 0 deletions tests/integration/cli/env/.env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FOO=5
BAR=6
57 changes: 57 additions & 0 deletions tests/integration/cli/env/env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { beforeEach, describe, expect, test } from 'vitest';

const localFile = path.join(__dirname, '.env.local');
const prodLocalFile = path.join(__dirname, '.env.production.local');

describe('load env file', async () => {
beforeEach(() => {
fs.rmSync(localFile, { force: true });
fs.rmSync(prodLocalFile, { force: true });
});

test('should load .env config and allow rslib.config.ts to read env vars', async () => {
execSync('npx rslib build', {
cwd: __dirname,
});

expect(fs.existsSync(path.join(__dirname, 'dist/1'))).toBeTruthy();
});

test('should load .env.local with higher priority', async () => {
fs.writeFileSync(localFile, 'FOO=2');

execSync('npx rslib build', {
cwd: __dirname,
});
expect(fs.existsSync(path.join(__dirname, 'dist/2'))).toBeTruthy();
});

test('should load .env.production.local with higher priority', async () => {
fs.writeFileSync(localFile, 'FOO=2');
fs.writeFileSync(prodLocalFile, 'FOO=3');

execSync('npx rslib build', {
cwd: __dirname,
});
expect(fs.existsSync(path.join(__dirname, 'dist/3'))).toBeTruthy();
});

test('should allow to specify env mode via --env-mode', async () => {
execSync('npx rslib build --env-mode test', {
cwd: __dirname,
});

expect(fs.existsSync(path.join(__dirname, 'dist/5'))).toBeTruthy();
});

test('should allow to custom env directory via --env-dir', async () => {
execSync('npx rslib build --env-dir env', {
cwd: __dirname,
});

expect(fs.existsSync(path.join(__dirname, 'dist/7'))).toBeTruthy();
});
});
2 changes: 2 additions & 0 deletions tests/integration/cli/env/env/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FOO=7
BAR=8
6 changes: 6 additions & 0 deletions tests/integration/cli/env/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "cli-env-test",
"version": "1.0.0",
"private": true,
"type": "module"
}
14 changes: 14 additions & 0 deletions tests/integration/cli/env/rslib.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defineConfig } from '@rslib/core';
import { generateBundleEsmConfig } from 'test-helper';

export default defineConfig({
lib: [
generateBundleEsmConfig({
output: {
distPath: {
root: `dist/${process.env.FOO}`,
},
},
}),
],
});
1 change: 1 addition & 0 deletions tests/integration/cli/env/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const foo = 'foo';
84 changes: 84 additions & 0 deletions website/docs/en/guide/basic/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,93 @@ Options:
-c --config <config> specify the configuration file, can be a relative or absolute path
-r --root <root> specify the project root directory, can be an absolute path or a path relative to cwd
--env-mode <mode> specify the env mode to load the `.env.[mode]` file
--env-dir <dir> specify the directory to load `.env` files
--lib <id> build the specified library (may be repeated)
-w --watch turn on watch mode, watch for changes and rebuild
-h, --help display help for command
```

### Environment Variables

Rslib supports injecting env variables or expressions into the code during build, which is helpful for distinguishing the running environment or replacing constants.

You can see more details in [Environment Variables](https://rsbuild.dev/guide/advanced/env-vars#environment-variables).

::: note

`process.env.NODE_ENV` will be preserved in the build output when [format](/config/lib/format) is set to `esm` or `cjs`. For `mf` format, it will be preserved to make output directly usable.

:::

#### Env Mode

Rslib supports reading `.env.[mode]` and `.env.[mode].local` files. You can specify the env mode using the `--env-mode <mode>` flag.

For example, set the env mode as `test`:

```bash
npx rslib build --env-mode test
```

Rslib will then read the following files in sequence:

- `.env`
- `.env.local`
- `.env.test`
- `.env.test.local`

:::tip

The `--env-mode` option takes precedence over `process.env.NODE_ENV`.

It is recommended to use `--env-mode` to set the env mode, and not to modify `process.env.NODE_ENV`.

:::

#### Env Directory

By default, the `.env` file is located in the root directory of the project. You can specify the env directory by using the `--env-dir <dir>` option in the CLI.

For example, to specify the env directory as `config`:

```bash
npx rslib build --env-dir config
```

In this case, Rslib will read the `./config/.env` and other env files.

##### Example

For example, create a `.env` file and add the following contents:

```shell title=".env"
FOO=hello
BAR=1
```

Then in the `rslib.config.ts` file, you can access the above env variables using `import.meta.env.[name]` or `process.env.[name]`:

```ts title="rslib.config.ts"
console.log(import.meta.env.FOO); // 'hello'
console.log(import.meta.env.BAR); // '1'

console.log(process.env.FOO); // 'hello'
console.log(process.env.BAR); // '1'
```

Now, create a `.env.local` file and add the following contents:

```shell title=".env.local"
BAR=2
```

The value of `BAR` is overwritten to `'2'`:

```ts title="rslib.config.ts"
console.log(import.meta.env.BAR); // '2'
console.log(process.env.BAR); // '2'
```

### Watch Mode

You can use `rslib build --watch` to turn on watch mode for watching for changes and rebuild.
Expand All @@ -65,6 +147,7 @@ Options:
-c --config <config> specify the configuration file, can be a relative or absolute path
-r --root <root> specify the project root directory, can be an absolute path or a path relative to cwd
--env-mode <mode> specify the env mode to load the `.env.[mode]` file
--env-dir <dir> specify the directory to load `.env` files
--lib <id> inspect the specified library (may be repeated)
--output <output> specify inspect content output path (default: ".rsbuild")
--verbose show full function definitions in output
Expand Down Expand Up @@ -127,5 +210,6 @@ Options:
-c --config <config> specify the configuration file, can be a relative or absolute path
-r --root <root> specify the project root directory, can be an absolute path or a path relative to cwd
--env-mode <mode> specify the env mode to load the `.env.[mode]` file
--env-dir <dir> specify the directory to load `.env` files
-h, --help display help for command
```
Loading