From e9f38524ecd8b82ff77c897a2fbb526da0833e4c Mon Sep 17 00:00:00 2001 From: Timeless0911 <1604889533@qq.com> Date: Wed, 4 Dec 2024 17:19:25 +0800 Subject: [PATCH] feat: support load env --- .gitignore | 2 + packages/core/src/cli/commands.ts | 24 +++---- packages/core/src/cli/init.ts | 41 +++++++++-- pnpm-lock.yaml | 2 + tests/integration/cli/env/.env | 2 + tests/integration/cli/env/.env.test | 2 + tests/integration/cli/env/env.test.ts | 57 +++++++++++++++ tests/integration/cli/env/env/.env | 2 + tests/integration/cli/env/package.json | 6 ++ tests/integration/cli/env/rslib.config.ts | 14 ++++ tests/integration/cli/env/src/index.ts | 1 + website/docs/en/guide/basic/cli.mdx | 84 +++++++++++++++++++++++ 12 files changed, 221 insertions(+), 16 deletions(-) create mode 100644 tests/integration/cli/env/.env create mode 100644 tests/integration/cli/env/.env.test create mode 100644 tests/integration/cli/env/env.test.ts create mode 100644 tests/integration/cli/env/env/.env create mode 100644 tests/integration/cli/env/package.json create mode 100644 tests/integration/cli/env/rslib.config.ts create mode 100644 tests/integration/cli/env/src/index.ts diff --git a/.gitignore b/.gitignore index ca788a553..aaededd18 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ test-results .nx/ **/@mf-types **/@mf-types/** +.env.local +.env.*.local \ No newline at end of file diff --git a/packages/core/src/cli/commands.ts b/packages/core/src/cli/commands.ts index 32720dab8..0fe0323b1 100644 --- a/packages/core/src/cli/commands.ts +++ b/packages/core/src/cli/commands.ts @@ -2,7 +2,7 @@ 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'; @@ -10,6 +10,7 @@ import { watchFilesForRestart } from './restart'; export type CommonOptions = { root?: string; config?: string; + envDir?: string; envMode?: string; lib?: string[]; }; @@ -37,7 +38,8 @@ const applyCommonOptions = (command: Command) => { .option( '--env-mode ', 'specify the env mode to load the `.env.[mode]` file', - ); + ) + .option('--env-dir ', 'specify the directory to load `.env` files'); }; const repeatableOption = (value: string, previous: string[]) => { @@ -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(); }); } @@ -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, @@ -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(); }); }; diff --git a/packages/core/src/cli/init.ts b/packages/core/src/cli/init.ts index 555d177ba..c737efa7f 100644 --- a/packages/core/src/cli/init.ts +++ b/packages/core/src/cli/init.ts @@ -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], + }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc46e137c..3bdc06e72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -551,6 +551,8 @@ importers: tests/integration/cli/build/custom-root: {} + tests/integration/cli/env: {} + tests/integration/cli/inspect: {} tests/integration/copy: {} diff --git a/tests/integration/cli/env/.env b/tests/integration/cli/env/.env new file mode 100644 index 000000000..d5e31f23c --- /dev/null +++ b/tests/integration/cli/env/.env @@ -0,0 +1,2 @@ +FOO=1 +BAR=2 \ No newline at end of file diff --git a/tests/integration/cli/env/.env.test b/tests/integration/cli/env/.env.test new file mode 100644 index 000000000..5133c1f22 --- /dev/null +++ b/tests/integration/cli/env/.env.test @@ -0,0 +1,2 @@ +FOO=5 +BAR=6 \ No newline at end of file diff --git a/tests/integration/cli/env/env.test.ts b/tests/integration/cli/env/env.test.ts new file mode 100644 index 000000000..44b9f1a1e --- /dev/null +++ b/tests/integration/cli/env/env.test.ts @@ -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(); + }); +}); diff --git a/tests/integration/cli/env/env/.env b/tests/integration/cli/env/env/.env new file mode 100644 index 000000000..3edfa70da --- /dev/null +++ b/tests/integration/cli/env/env/.env @@ -0,0 +1,2 @@ +FOO=7 +BAR=8 \ No newline at end of file diff --git a/tests/integration/cli/env/package.json b/tests/integration/cli/env/package.json new file mode 100644 index 000000000..c2fafed33 --- /dev/null +++ b/tests/integration/cli/env/package.json @@ -0,0 +1,6 @@ +{ + "name": "cli-env-test", + "version": "1.0.0", + "private": true, + "type": "module" +} diff --git a/tests/integration/cli/env/rslib.config.ts b/tests/integration/cli/env/rslib.config.ts new file mode 100644 index 000000000..e261e5e88 --- /dev/null +++ b/tests/integration/cli/env/rslib.config.ts @@ -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}`, + }, + }, + }), + ], +}); diff --git a/tests/integration/cli/env/src/index.ts b/tests/integration/cli/env/src/index.ts new file mode 100644 index 000000000..3329a7d97 --- /dev/null +++ b/tests/integration/cli/env/src/index.ts @@ -0,0 +1 @@ +export const foo = 'foo'; diff --git a/website/docs/en/guide/basic/cli.mdx b/website/docs/en/guide/basic/cli.mdx index e0fe2e31e..c3fb989f6 100644 --- a/website/docs/en/guide/basic/cli.mdx +++ b/website/docs/en/guide/basic/cli.mdx @@ -39,11 +39,93 @@ Options: -c --config specify the configuration file, can be a relative or absolute path -r --root specify the project root directory, can be an absolute path or a path relative to cwd --env-mode specify the env mode to load the `.env.[mode]` file + --env-dir specify the directory to load `.env` files --lib 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 ` 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 ` 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. @@ -65,6 +147,7 @@ Options: -c --config specify the configuration file, can be a relative or absolute path -r --root specify the project root directory, can be an absolute path or a path relative to cwd --env-mode specify the env mode to load the `.env.[mode]` file + --env-dir specify the directory to load `.env` files --lib inspect the specified library (may be repeated) --output specify inspect content output path (default: ".rsbuild") --verbose show full function definitions in output @@ -127,5 +210,6 @@ Options: -c --config specify the configuration file, can be a relative or absolute path -r --root specify the project root directory, can be an absolute path or a path relative to cwd --env-mode specify the env mode to load the `.env.[mode]` file + --env-dir specify the directory to load `.env` files -h, --help display help for command ```