diff --git a/e2e/cases/javascript-api/build-and-load-env/.env b/e2e/cases/javascript-api/build-and-load-env/.env new file mode 100644 index 0000000000..54bd736db0 --- /dev/null +++ b/e2e/cases/javascript-api/build-and-load-env/.env @@ -0,0 +1 @@ +PUBLIC_FOO=foo diff --git a/e2e/cases/javascript-api/build-and-load-env/.env.prod b/e2e/cases/javascript-api/build-and-load-env/.env.prod new file mode 100644 index 0000000000..22028fcb64 --- /dev/null +++ b/e2e/cases/javascript-api/build-and-load-env/.env.prod @@ -0,0 +1 @@ +PUBLIC_BAR=bar diff --git a/e2e/cases/javascript-api/build-and-load-env/index.test.ts b/e2e/cases/javascript-api/build-and-load-env/index.test.ts new file mode 100644 index 0000000000..18d12961c2 --- /dev/null +++ b/e2e/cases/javascript-api/build-and-load-env/index.test.ts @@ -0,0 +1,46 @@ +import { expect } from '@playwright/test'; +import { createRsbuild } from '@rsbuild/core'; +import { rspackOnlyTest } from 'scripts'; + +rspackOnlyTest('should not load env by default', async () => { + const rsbuild = await createRsbuild({ + cwd: __dirname, + loadEnv: false, + rsbuildConfig: { + performance: { + printFileSize: false, + }, + }, + }); + + expect(process.env.PUBLIC_FOO).toBe(undefined); + expect(process.env.PUBLIC_BAR).toBe(undefined); + const { close } = await rsbuild.build(); + await close(); +}); + +rspackOnlyTest( + 'should allow to call `build` with `loadEnv` options', + async () => { + const rsbuild = await createRsbuild({ + cwd: __dirname, + loadEnv: { + mode: 'prod', + }, + rsbuildConfig: { + performance: { + printFileSize: false, + }, + }, + }); + + expect(process.env.PUBLIC_FOO).toBe('foo'); + expect(process.env.PUBLIC_BAR).toBe('bar'); + + const { close } = await rsbuild.build(); + await close(); + + expect(process.env.PUBLIC_FOO).toBe(undefined); + expect(process.env.PUBLIC_BAR).toBe(undefined); + }, +); diff --git a/e2e/cases/javascript-api/build-and-load-env/src/index.js b/e2e/cases/javascript-api/build-and-load-env/src/index.js new file mode 100644 index 0000000000..e921523b1b --- /dev/null +++ b/e2e/cases/javascript-api/build-and-load-env/src/index.js @@ -0,0 +1 @@ +console.log('hello'); diff --git a/packages/core/src/cli/init.ts b/packages/core/src/cli/init.ts index 0c779e604c..09a6ab845d 100644 --- a/packages/core/src/cli/init.ts +++ b/packages/core/src/cli/init.ts @@ -2,7 +2,6 @@ import path from 'node:path'; import { loadConfig as baseLoadConfig, watchFilesForRestart } from '../config'; import { createRsbuild } from '../createRsbuild'; import { castArray, getAbsolutePath } from '../helpers'; -import { type LoadEnvResult, loadEnv } from '../loadEnv'; import { logger } from '../logger'; import type { RsbuildInstance } from '../types'; import type { CommonOptions } from './commands'; @@ -16,22 +15,19 @@ const getEnvDir = (cwd: string, envDir?: string) => { return cwd; }; -const loadConfig = async (root: string, envs: LoadEnvResult) => { - const { content: config } = await baseLoadConfig({ +const loadConfig = async (root: string) => { + const { content: config, filePath } = await baseLoadConfig({ cwd: root, path: commonOpts.config, envMode: commonOpts.envMode, loader: commonOpts.configLoader, }); + config.dev ||= {}; config.source ||= {}; - config.source.define = { - ...envs.publicVars, - ...config.source.define, - }; + config.server ||= {}; if (commonOpts.base) { - config.server ||= {}; config.server.base = commonOpts.base; } @@ -44,38 +40,31 @@ const loadConfig = async (root: string, envs: LoadEnvResult) => { } if (commonOpts.open && !config.server?.open) { - config.server ||= {}; config.server.open = commonOpts.open; } if (commonOpts.host) { - config.server ||= {}; config.server.host = commonOpts.host; } if (commonOpts.port) { - config.server ||= {}; config.server.port = commonOpts.port; } // enable CLI shortcuts by default when using Rsbuild CLI - if (config.dev?.cliShortcuts === undefined) { - config.dev ||= {}; + if (config.dev.cliShortcuts === undefined) { config.dev.cliShortcuts = true; } - // add env files to build dependencies, so that the build cache - // can be invalidated when the env files are changed. - if (config.performance?.buildCache && envs.filePaths.length > 0) { - const { buildCache } = config.performance; - if (buildCache === true) { - config.performance.buildCache = { - buildDependencies: envs.filePaths, - }; - } else { - buildCache.buildDependencies ||= []; - buildCache.buildDependencies.push(...envs.filePaths); - } + // watch the config file + if (filePath) { + config.dev.watchFiles = [ + ...(config.dev.watchFiles ? castArray(config.dev.watchFiles) : []), + { + paths: filePath, + type: 'reload-server', + }, + ]; } return config; @@ -97,28 +86,23 @@ export async function init({ try { const cwd = process.cwd(); const root = commonOpts.root ? getAbsolutePath(cwd, commonOpts.root) : cwd; - const envs = loadEnv({ - cwd: getEnvDir(root, commonOpts.envDir), - mode: commonOpts.envMode, - }); const rsbuild = await createRsbuild({ cwd: root, - rsbuildConfig: async () => loadConfig(root, envs), + rsbuildConfig: () => loadConfig(root), environment: commonOpts.environment, + loadEnv: { + cwd: getEnvDir(root, commonOpts.envDir), + mode: commonOpts.envMode, + }, }); rsbuild.onBeforeCreateCompiler(() => { const command = process.argv[2]; if (command === 'dev' || isBuildWatch) { - const files = [...envs.filePaths]; - - const { _privateMeta } = rsbuild.getNormalizedConfig(); - if (_privateMeta) { - files.push(_privateMeta.configFilePath); - } - + const files: string[] = []; const config = rsbuild.getNormalizedConfig(); + if (config.dev?.watchFiles) { for (const watchFilesConfig of castArray(config.dev.watchFiles)) { if (watchFilesConfig.type !== 'reload-server') { @@ -143,9 +127,6 @@ export async function init({ } }); - rsbuild.onCloseBuild(envs.cleanup); - rsbuild.onCloseDevServer(envs.cleanup); - return rsbuild; } catch (err) { if (isRestart) { diff --git a/packages/core/src/createRsbuild.ts b/packages/core/src/createRsbuild.ts index 50f34e3015..c4cd959118 100644 --- a/packages/core/src/createRsbuild.ts +++ b/packages/core/src/createRsbuild.ts @@ -2,6 +2,7 @@ import { existsSync } from 'node:fs'; import { isPromise } from 'node:util/types'; import { createContext } from './createContext'; import { + castArray, color, getNodeEnv, isEmptyDir, @@ -10,6 +11,7 @@ import { setNodeEnv, } from './helpers'; import { initPluginAPI } from './initPlugins'; +import { type LoadEnvResult, loadEnv } from './loadEnv'; import { logger } from './logger'; import { createPluginManager } from './pluginManager'; import { pluginAppIcon } from './plugins/appIcon'; @@ -59,6 +61,7 @@ import type { PluginManager, PreviewOptions, ResolvedCreateRsbuildOptions, + RsbuildConfig, RsbuildInstance, RsbuildPlugin, RsbuildPlugins, @@ -114,25 +117,75 @@ async function applyDefaultPlugins( ]); } +function applyEnvsToConfig(config: RsbuildConfig, envs: LoadEnvResult | null) { + if (envs === null) { + return; + } + + // define the public env variables + config.source ||= {}; + config.source.define = { + ...envs.publicVars, + ...config.source.define, + }; + + if (envs.filePaths.length === 0) { + return; + } + + // watch the env files + config.dev ||= {}; + config.dev.watchFiles = [ + ...(config.dev.watchFiles ? castArray(config.dev.watchFiles) : []), + { + paths: envs.filePaths, + type: 'reload-server', + }, + ]; + + // add env files to build dependencies, so that the build cache + // can be invalidated when the env files are changed. + if (config.performance?.buildCache) { + const { buildCache } = config.performance; + if (buildCache === true) { + config.performance.buildCache = { + buildDependencies: envs.filePaths, + }; + } else { + buildCache.buildDependencies ||= []; + buildCache.buildDependencies.push(...envs.filePaths); + } + } +} + /** * Create an Rsbuild instance. */ export async function createRsbuild( options: CreateRsbuildOptions = {}, ): Promise { - const rsbuildConfig = isFunction(options.rsbuildConfig) + const envs = options.loadEnv + ? loadEnv({ + cwd: options.cwd, + ...(typeof options.loadEnv === 'boolean' ? {} : options.loadEnv), + }) + : null; + + const config = isFunction(options.rsbuildConfig) ? await options.rsbuildConfig() : options.rsbuildConfig || {}; + applyEnvsToConfig(config, envs); + const resolvedOptions: ResolvedCreateRsbuildOptions = { cwd: process.cwd(), ...options, - rsbuildConfig, + rsbuildConfig: config, }; const pluginManager = createPluginManager(); - const context = await createContext(resolvedOptions, rsbuildConfig); + const context = await createContext(resolvedOptions, config); const getPluginAPI = initPluginAPI({ context, pluginManager }); context.getPluginAPI = getPluginAPI; @@ -142,8 +195,7 @@ export async function createRsbuild( await applyDefaultPlugins(pluginManager, context); logger.debug('add default plugins done'); - const provider = - (rsbuildConfig.provider as RsbuildProvider) || rspackProvider; + const provider = (config.provider as RsbuildProvider) || rspackProvider; const providerInstance = await provider({ context, @@ -260,6 +312,11 @@ export async function createRsbuild( ...pick(providerInstance, ['initConfigs', 'inspectConfig']), }; + if (envs) { + rsbuild.onCloseBuild(envs.cleanup); + rsbuild.onCloseDevServer(envs.cleanup); + } + const getFlattenedPlugins = async (pluginOptions: RsbuildPlugins) => { let plugins = pluginOptions; do { @@ -271,32 +328,34 @@ export async function createRsbuild( return plugins as Array; }; - if (rsbuildConfig.plugins) { - const plugins = await getFlattenedPlugins(rsbuildConfig.plugins); + if (config.plugins) { + const plugins = await getFlattenedPlugins(config.plugins); rsbuild.addPlugins(plugins); } // Register environment plugin - if (rsbuildConfig.environments) { + if (config.environments) { await Promise.all( - Object.entries(rsbuildConfig.environments).map(async ([name, config]) => { - if (!config.plugins) { - return; - } - - // If the current environment is not specified, skip it - if ( - context.specifiedEnvironments && - !context.specifiedEnvironments.includes(name) - ) { - return; - } - - const plugins = await getFlattenedPlugins(config.plugins); - rsbuild.addPlugins(plugins, { - environment: name, - }); - }), + Object.entries(config.environments).map( + async ([name, environmentConfig]) => { + if (!environmentConfig.plugins) { + return; + } + + // If the current environment is not specified, skip it + if ( + context.specifiedEnvironments && + !context.specifiedEnvironments.includes(name) + ) { + return; + } + + const plugins = await getFlattenedPlugins(environmentConfig.plugins); + rsbuild.addPlugins(plugins, { + environment: name, + }); + }, + ), ); } diff --git a/packages/core/src/types/rsbuild.ts b/packages/core/src/types/rsbuild.ts index 1fe09540b1..74e23daa5a 100644 --- a/packages/core/src/types/rsbuild.ts +++ b/packages/core/src/types/rsbuild.ts @@ -1,4 +1,5 @@ import type { Compiler, MultiCompiler } from '@rspack/core'; +import type { LoadEnvOptions } from '../loadEnv'; import type * as providerHelpers from '../provider/helpers'; import type { RsbuildDevServer } from '../server/devServer'; import type { StartServerResult } from '../server/helper'; @@ -126,14 +127,20 @@ export type CreateRsbuildOptions = { * Passing a function to load the config asynchronously with custom logic. */ rsbuildConfig?: RsbuildConfig | (() => Promise); + /** + * Whether to call `loadEnv` to load environment variables and define them + * as global variables. + * @default false + */ + loadEnv?: boolean | LoadEnvOptions; }; export type ResolvedCreateRsbuildOptions = Required< - Omit -> & { - rsbuildConfig: RsbuildConfig; - environment?: CreateRsbuildOptions['environment']; -}; + Pick +> & + Pick & { + rsbuildConfig: RsbuildConfig; + }; export type CreateDevServer = ( options?: CreateDevServerOptions,