Skip to content

Commit

Permalink
feat(cli): support reload .env files and restart dev server (#767)
Browse files Browse the repository at this point in the history
  • Loading branch information
chenjiahan authored Nov 29, 2023
1 parent da80608 commit 49d9457
Show file tree
Hide file tree
Showing 13 changed files with 98 additions and 27 deletions.
5 changes: 5 additions & 0 deletions .changeset/tender-months-explode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rsbuild/core': patch
---

feat(cli): support reload .env files and restart dev server
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { exec } from 'child_process';
import { test } from '@playwright/test';
import { fse } from '@rsbuild/shared';
import { awaitFileExists } from '@scripts/helper';
import { getRandomPort } from '@scripts/shared';

test('should restart dev server and reload config when config file changed', async () => {
const dist1 = path.join(__dirname, 'dist');
Expand All @@ -20,6 +21,7 @@ test('should restart dev server and reload config when config file changed', asy
root: 'dist',
},
},
server: { port: ${getRandomPort()} }
};`,
);

Expand All @@ -37,6 +39,7 @@ test('should restart dev server and reload config when config file changed', asy
root: 'dist-2',
},
},
server: { port: ${getRandomPort()} }
};`,
);

Expand Down
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions e2e/cases/cli/reload-env/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rsbuild.config.mjs
44 changes: 44 additions & 0 deletions e2e/cases/cli/reload-env/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import path from 'path';
import { exec } from 'child_process';
import { test, expect } from '@playwright/test';
import { fse } from '@rsbuild/shared';
import { awaitFileExists } from '@scripts/helper';
import { getRandomPort } from '@scripts/shared';

test('should restart dev server when .env file is changed', async () => {
const dist = path.join(__dirname, 'dist');
const configFile = path.join(__dirname, 'rsbuild.config.mjs');
const envLocalFile = path.join(__dirname, '.env.local');
const distIndex = path.join(dist, 'static/js/index.js');
fse.removeSync(dist);
fse.removeSync(configFile);
fse.removeSync(envLocalFile);

fse.writeFileSync(envLocalFile, ``);
fse.writeFileSync(
configFile,
`export default {
output: {
distPath: {
root: 'dist',
},
disableFilenameHash: true,
},
server: { port: ${getRandomPort()} }
};`,
);

const process = exec('npx rsbuild dev', {
cwd: __dirname,
});

await awaitFileExists(distIndex);
expect(fse.readFileSync(distIndex, 'utf-8')).not.toContain('jack');
fse.removeSync(distIndex);

fse.writeFileSync(envLocalFile, `PUBLIC_NAME=jack`);
await awaitFileExists(distIndex);
expect(fse.readFileSync(distIndex, 'utf-8')).toContain('jack');

process.kill();
});
1 change: 1 addition & 0 deletions e2e/cases/cli/reload-env/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('hello!', process.env.PUBLIC_NAME);
12 changes: 12 additions & 0 deletions e2e/cases/cli/reload-env/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "@rsbuild/tsconfig/base",
"compilerOptions": {
"jsx": "react-jsx",
"baseUrl": "./",
"outDir": "./dist",
"paths": {
"@scripts/*": ["../../../scripts/*"]
}
},
"include": ["src", "*.test.ts"]
}
16 changes: 9 additions & 7 deletions e2e/scripts/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,18 @@ export const globContentJSON = async (path: string, options?: GlobOptions) => {
return ret;
};

export const awaitFileExists = async (dir: string, checks = 0) => {
export const awaitFileExists = async (dir: string) => {
const maxChecks = 100;
const interval = 100;
if (fse.existsSync(dir)) {
expect(true).toBe(true);
} else if (checks < maxChecks) {
let checks = 0;

while (checks < maxChecks) {
if (fse.existsSync(dir)) {
return;
}
checks++;
await new Promise((resolve) => setTimeout(resolve, interval));
await awaitFileExists(dir, checks);
} else {
expect(false).toBe(true);
}

throw new Error('awaitFileExists failed: ' + dir);
};
6 changes: 4 additions & 2 deletions packages/core/src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ export async function init({
}

try {
const { publicVars } = await loadEnv();
const config = await loadConfig(commonOpts.config);
const root = process.cwd();
const { publicVars } = await loadEnv({ dir: root });
const config = await loadConfig(root, commonOpts.config);
const { createRsbuild } = await import('../createRsbuild');

config.source ||= {};
Expand All @@ -66,6 +67,7 @@ export async function init({
}

return await createRsbuild({
cwd: root,
rsbuildConfig: config,
provider: config.provider,
});
Expand Down
22 changes: 13 additions & 9 deletions packages/core/src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
debounce,
type RsbuildConfig as BaseRsbuildConfig,
} from '@rsbuild/shared';
import { getEnvFiles } from '../loadEnv';
import { restartDevServer } from '../server/restart';

export type RsbuildConfig = BaseRsbuildConfig & {
Expand Down Expand Up @@ -36,9 +37,7 @@ export function defineConfig(config: RsbuildConfigExport) {
return config;
}

const resolveConfigPath = (customConfig?: string) => {
const root = process.cwd();

const resolveConfigPath = (root: string, customConfig?: string) => {
if (customConfig) {
const customConfigPath = isAbsolute(customConfig)
? customConfig
Expand Down Expand Up @@ -69,31 +68,36 @@ const resolveConfigPath = (customConfig?: string) => {
return null;
};

async function watchConfig(configFile: string) {
async function watchConfig(root: string, configFile: string) {
const chokidar = await import('@rsbuild/shared/chokidar');
const envFiles = getEnvFiles().map((filename) => join(root, filename));

const watcher = chokidar.watch(configFile, {
const watcher = chokidar.watch([configFile, ...envFiles], {
// do not trigger add for initial files
ignoreInitial: true,
// If watching fails due to read permissions, the errors will be suppressed silently.
ignorePermissionErrors: true,
});

const callback = debounce(
async () => {
async (filePath) => {
watcher.close();
await restartDevServer({ filePath: configFile });
await restartDevServer({ filePath });
},
// set 300ms debounce to avoid restart frequently
300,
);

watcher.on('add', callback);
watcher.on('change', callback);
watcher.on('unlink', callback);
}

export async function loadConfig(
root: string,
customConfig?: string,
): Promise<RsbuildConfig> {
const configFile = resolveConfigPath(customConfig);
const configFile = resolveConfigPath(root, customConfig);

if (!configFile) {
return {};
Expand All @@ -110,7 +114,7 @@ export async function loadConfig(

const command = process.argv[2];
if (command === 'dev') {
watchConfig(configFile);
watchConfig(root, configFile);
}

const configExport = loadConfig(configFile) as RsbuildConfigExport;
Expand Down
15 changes: 6 additions & 9 deletions packages/core/src/loadEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,19 @@ import fs from 'fs';
import { join } from 'path';
import { isFileSync } from '@rsbuild/shared';

export const getEnvFiles = () => {
const { NODE_ENV } = process.env;
return ['.env', '.env.local', `.env.${NODE_ENV}`, `.env.${NODE_ENV}.local`];
};

export async function loadEnv({
dir = process.cwd(),
prefixes = ['PUBLIC_'],
}: { dir?: string; prefixes?: string[] } = {}) {
const { parse } = await import('../compiled/dotenv');
const { expand } = await import('../compiled/dotenv-expand');

const { NODE_ENV } = process.env;
const files = [
'.env',
'.env.local',
`.env.${NODE_ENV}`,
`.env.${NODE_ENV}.local`,
];

const envPaths = files
const envPaths = getEnvFiles()
.map((filename) => join(dir, filename))
.filter(isFileSync);

Expand Down

0 comments on commit 49d9457

Please sign in to comment.