Skip to content
Open
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
9 changes: 9 additions & 0 deletions .changeset/icy-ravens-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@modern-js/app-tools': patch
'@modern-js/plugin': patch
'@modern-js/utils': patch
'@modern-js/server-core': patch
---

feat: add --env-dir support across dev/build/serve
feat: 给 dev/build/serve 支持 --env-dir
22 changes: 22 additions & 0 deletions packages/document/docs/en/apis/app/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Usage: modern dev [options]
Options:
-e --entry <entry> compiler by entry
-c --config <config> specify the configuration file, which can be a relative or absolute path
--env-dir <dir> specify the directory containing .env files
-h, --help show command help
--web-only only start web service
--api-only only start API service
Expand Down Expand Up @@ -76,10 +77,29 @@ Usage: modern build [options]

Options:
-c --config <config> specify the configuration file, which can be a relative or absolute path
--env-dir <dir> specify the directory containing .env files
-h, --help show command help
-w --watch turn on watch mode, watch for changes and rebuild
```

### Specify Environment Directory

You can use `--env-dir` to load `.env` files from a custom directory instead of the project root.

```bash
# Load from <projectRoot>/env/.env and .env.development
modern dev --env-dir ./env

# Load from <projectRoot>/env/.env and .env.production
modern build --env-dir ./env
modern serve --env-dir ./env
modern deploy --env-dir ./env
```

By default, Modern.js still loads `.env*` from the project root when this option is not provided.

The path of `--env-dir` is resolved relative to the project root.

## modern new

The `modern new` command is used to enable features in an existing project.
Expand Down Expand Up @@ -133,6 +153,8 @@ import ServeCommand from '@site-docs-en/components/serve-command';

<ServeCommand />

`modern serve` also supports `--env-dir <dir>` to load `.env*` from a custom directory.

## modern upgrade

Execute the command `npx modern upgrade` in the project, by default, dependencies in the `package.json` are updated to the latest version.
Expand Down
1 change: 1 addition & 0 deletions packages/document/docs/en/components/deploy-command.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Usage: modern deploy [options]

Options:
-c --config <config> Specify configuration file path, either relative or absolute
--env-dir <dir> Specify the directory containing .env files
-s --skip-build Skip the build stage
-h, --help Display command help
```
Expand Down
22 changes: 22 additions & 0 deletions packages/document/docs/zh/apis/app/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Usage: modern dev [options]
Options:
-e --entry <entry> 指定入口,只编译特定的页面
-c --config <config> 指定配置文件路径,可以为相对路径或绝对路径
--env-dir <dir> 指定 .env 文件所在目录
-h, --help 显示命令帮助
--web-only 仅启动 Web 服务
--api-only 仅启动 API 接口服务
Expand Down Expand Up @@ -76,10 +77,29 @@ Usage: modern build [options]

Options:
-c --config <config> 指定配置文件路径,可以为相对路径或绝对路径
--env-dir <dir> 指定 .env 文件所在目录
-h, --help 显示命令帮助
-w --watch 开启 watch 模式, 监听文件变更并重新构建
```

### 指定环境变量目录

你可以通过 `--env-dir` 指定 `.env` 文件目录,而不是默认从项目根目录读取。

```bash
# 从 <projectRoot>/env/.env 和 .env.development 加载
modern dev --env-dir ./env

# 从 <projectRoot>/env/.env 和 .env.production 加载
modern build --env-dir ./env
modern serve --env-dir ./env
modern deploy --env-dir ./env
```

当未传入该参数时,Modern.js 仍会默认从项目根目录加载 `.env*`。

`--env-dir` 的路径相对于项目根目录解析。

## modern new

`modern new` 命令用于在已有项目中添加项目元素。
Expand Down Expand Up @@ -133,6 +153,8 @@ import ServeCommand from '@site-docs/components/serve-command';

<ServeCommand />

`modern serve` 同样支持 `--env-dir <dir>`,用于从指定目录加载 `.env*` 文件。

## modern upgrade

在项目根目录下执行命令 `npx modern upgrade`,会默认将当前执行命令项目的 `package.json` 中的 Modern.js 相关依赖更新至最新版本。
Expand Down
1 change: 1 addition & 0 deletions packages/document/docs/zh/components/deploy-command.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Usage: modern deploy [options]

Options:
-c --config <config> 指定配置文件路径,可以为相对路径或绝对路径
--env-dir <dir> 指定 .env 文件所在目录
-s --skip-build 跳过构建阶段
-h, --help 显示命令帮助
```
Expand Down
14 changes: 10 additions & 4 deletions packages/server/core/src/adapters/node/helper/loadEnv.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import path from 'path';
import { fs, dotenv, dotenvExpand } from '@modern-js/utils';
import {
fs,
dotenv,
dotenvExpand,
resolveInsideOrFallback,
} from '@modern-js/utils';
import type { ServerBaseOptions } from '../../../serverBase';

/** 读取 .env.{process.env.MODERN_ENV} 文件,加载环境变量 */
export async function loadServerEnv(options: ServerBaseOptions) {
const { pwd } = options;
const { pwd, envDir } = options;
const serverEnv = process.env.MODERN_ENV;
const defaultEnvPath = path.resolve(pwd, `.env`);
const serverEnvPath = path.resolve(pwd, `.env.${serverEnv}`);
const envDirectory = resolveInsideOrFallback(pwd, envDir, pwd);
const defaultEnvPath = path.resolve(envDirectory, `.env`);
const serverEnvPath = path.resolve(envDirectory, `.env.${serverEnv}`);

if (
(await fs.pathExists(defaultEnvPath)) &&
Expand Down
29 changes: 29 additions & 0 deletions packages/server/core/tests/adapters/loadEnv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { loadServerEnv } from '../../src/adapters/node';

describe('test load serve env file', () => {
const pwd = path.resolve(__dirname, '../fixtures', 'serverEnv');
const envPwd = path.resolve(__dirname, '../fixtures', 'serverEnvDir');

it('should load env correctly', async () => {
await loadServerEnv({
Expand Down Expand Up @@ -37,4 +38,32 @@ describe('test load serve env file', () => {
delete process.env.USER_NAME;
delete process.env.ENV;
});

it('should load env from custom envDir', async () => {
process.env.MODERN_ENV = 'prod';
await loadServerEnv({
pwd: envPwd,
envDir: 'env',
} as any);

expect(process.env.USER_NAME).toBe('dir_prod_root');
expect(process.env.ENV).toBe('dir_prod');

delete process.env.USER_NAME;
delete process.env.ENV;
});

it('should fallback to pwd when envDir escapes root', async () => {
process.env.MODERN_ENV = 'prod';
await loadServerEnv({
pwd,
envDir: '../serverEnvDir/env',
} as any);

expect(process.env.USER_NAME).toBe('prod_root');
expect(process.env.ENV).toBe('prod');

delete process.env.USER_NAME;
delete process.env.ENV;
});
});
1 change: 1 addition & 0 deletions packages/server/core/tests/fixtures/serverEnvDir/env/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
USER_NAME=dir_root
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
USER_NAME=dir_prod_root
ENV=dir_prod
66 changes: 61 additions & 5 deletions packages/solutions/app-tools/src/commands/build.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,71 @@
import path from 'node:path';
import type { CLIPluginAPI } from '@modern-js/plugin';
import { fs, type Alias, logger } from '@modern-js/utils';
import {
fs,
type Alias,
isPathInside,
logger,
resolveInsideOrFallback,
} from '@modern-js/utils';
import type { ConfigChain } from '@rsbuild/core';
import type { AppTools } from '../types';
import { loadServerPlugins } from '../utils/loadPlugins';
import { setupTsRuntime } from '../utils/register';
import { generateRoutes } from '../utils/routes';
import type { BuildOptions } from '../utils/types';

function getSafeDistTarget(
distDirectory: string,
envDir: string | undefined,
envFile: string,
): string {
if (!envDir) {
return path.resolve(distDirectory, envFile);
}

const resolvedTargetPath = path.resolve(distDirectory, envDir, envFile);
if (!isPathInside(distDirectory, resolvedTargetPath)) {
logger.warn(
`The env directory ${envDir} is outside dist directory, fallback to dist root`,
);
return path.resolve(distDirectory, envFile);
}

return resolvedTargetPath;
}

async function copyEnvFiles(
appDirectory: string,
distDirectory: string,
envDir?: string,
): Promise<void> {
try {
const files = await fs.readdir(appDirectory);
const envDirectory = resolveInsideOrFallback(
appDirectory,
envDir,
appDirectory,
);
if (
envDir &&
!isPathInside(appDirectory, path.resolve(appDirectory, envDir))
) {
logger.warn(
`The env directory ${envDir} is outside project root, fallback to project root`,
);
}

if (!(await fs.pathExists(envDirectory))) {
logger.debug(`Env directory does not exist: ${envDirectory}`);
return;
}

const envDirectoryStat = await fs.stat(envDirectory);
if (!envDirectoryStat.isDirectory()) {
logger.debug(`Env path is not a directory: ${envDirectory}`);
return;
}

const files = await fs.readdir(envDirectory);

const envFileRegex = /^\.env(\.[a-zA-Z0-9_-]+)*$/;
const envFiles = files.filter(file => envFileRegex.test(file));
Expand All @@ -24,8 +76,8 @@ async function copyEnvFiles(
}

const copyPromises = envFiles.map(async envFile => {
const sourcePath = path.resolve(appDirectory, envFile);
const targetPath = path.resolve(distDirectory, envFile);
const sourcePath = path.resolve(envDirectory, envFile);
const targetPath = getSafeDistTarget(distDirectory, envDir, envFile);

try {
const stat = await fs.stat(sourcePath);
Expand Down Expand Up @@ -113,7 +165,11 @@ export const build = async (
);
}
await appContext.builder.onAfterBuild(async () => {
return copyEnvFiles(appContext.appDirectory, appContext.distDirectory);
return copyEnvFiles(
appContext.appDirectory,
appContext.distDirectory,
options?.envDir,
);
});
await appContext.builder.build({
watch: options?.watch,
Expand Down
11 changes: 8 additions & 3 deletions packages/solutions/app-tools/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
DevOptions,
InfoOptions,
InspectOptions,
StartOptions,
} from '../utils/types';

export const devCommand = async (
Expand All @@ -20,6 +21,7 @@ export const devCommand = async (
.usage('[options]')
.description(i18n.t(localeKeys.command.dev.describe))
.option('-c --config <config>', i18n.t(localeKeys.command.shared.config))
.option('--env-dir <dir>', i18n.t(localeKeys.command.shared.envDir))
.option('-e --entry [entry...]', i18n.t(localeKeys.command.dev.entry))
.option('--analyze', i18n.t(localeKeys.command.shared.analyze))
.option('--api-only', i18n.t(localeKeys.command.dev.apiOnly))
Expand All @@ -39,6 +41,7 @@ export const buildCommand = async (
.usage('[options]')
.description(i18n.t(localeKeys.command.build.describe))
.option('-c --config <config>', i18n.t(localeKeys.command.shared.config))
.option('--env-dir <dir>', i18n.t(localeKeys.command.shared.envDir))
.option('--analyze', i18n.t(localeKeys.command.shared.analyze))
.option('-w --watch', i18n.t(localeKeys.command.build.watch))
.action(async (options: BuildOptions) => {
Expand All @@ -57,9 +60,10 @@ export const serverCommand = (
.description(i18n.t(localeKeys.command.serve.describe))
.option('--api-only', i18n.t(localeKeys.command.dev.apiOnly))
.option('-c --config <config>', i18n.t(localeKeys.command.shared.config))
.action(async () => {
.option('--env-dir <dir>', i18n.t(localeKeys.command.shared.envDir))
.action(async (options: StartOptions) => {
const { serve } = await import('./serve.js');
await serve(api);
await serve(api, options);
});
};

Expand All @@ -71,12 +75,13 @@ export const deployCommand = (
.command('deploy')
.usage('[options]')
.option('-c --config <config>', i18n.t(localeKeys.command.shared.config))
.option('--env-dir <dir>', i18n.t(localeKeys.command.shared.envDir))
.option('-s --skip-build', i18n.t(localeKeys.command.shared.skipBuild))
.description(i18n.t(localeKeys.command.deploy.describe))
.action(async (options: DeployOptions) => {
if (!options.skipBuild) {
const { build } = await import('./build.js');
await build(api);
await build(api, options);
}

const { deploy } = await import('./deploy.js');
Expand Down
3 changes: 3 additions & 0 deletions packages/solutions/app-tools/src/commands/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import {
import type { AppNormalizedConfig, AppTools } from '../types';
import { loadServerPlugins } from '../utils/loadPlugins';
import { printInstructions } from '../utils/printInstructions';
import type { StartOptions } from '../utils/types';

type ExtraServerOptions = {
launcher?: typeof createProdServer;
};

export const serve = async (
api: CLIPluginAPI<AppTools>,
options?: StartOptions,
serverOptions?: ExtraServerOptions,
) => {
Comment thread
GiveMe-A-Name marked this conversation as resolved.
const appContext = api.getAppContext();
Expand Down Expand Up @@ -96,6 +98,7 @@ export const serve = async (
),
bffRuntimeFramework: appContext.bffRuntimeFramework,
},
envDir: options?.envDir,
runMode,
});

Expand Down
1 change: 1 addition & 0 deletions packages/solutions/app-tools/src/locale/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const EN_LOCALE = {
'specify the configuration file, which can be a relative or absolute path',
skipBuild: 'skip the build phase',
noNeedInstall: 'not run install command',
envDir: 'specify the directory containing .env files',
},
dev: {
describe: 'starting the dev server',
Expand Down
1 change: 1 addition & 0 deletions packages/solutions/app-tools/src/locale/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const ZH_LOCALE = {
config: '指定配置文件路径,可以为相对路径或绝对路径',
skipBuild: '跳过构建阶段',
noNeedInstall: '无需安装依赖',
envDir: '指定 .env 文件所在目录',
},
dev: {
describe: '启动开发服务器',
Expand Down
Loading
Loading