Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
21 changes: 21 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,28 @@ 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
```

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 +152,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
21 changes: 21 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,28 @@ 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.js 仍会默认从项目根目录加载 `.env*`。

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

## modern new

`modern new` 命令用于在已有项目中添加项目元素。
Expand Down Expand Up @@ -133,6 +152,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
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
8 changes: 6 additions & 2 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 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
3 changes: 3 additions & 0 deletions packages/solutions/app-tools/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ export type DevOptions = {
config?: string;
apiOnly?: boolean;
analyze?: boolean;
envDir?: string;
};

export type BuildOptions = {
config?: string;
analyze?: boolean;
watch?: boolean;
envDir?: string;
};

export type DeployOptions = {
Expand All @@ -18,6 +20,7 @@ export type DeployOptions = {

export type StartOptions = {
apiOnly?: boolean;
envDir?: string;
};

export type InspectOptions = {
Expand Down
Loading
Loading