Skip to content

Commit 8c329d2

Browse files
committed
feat: restart
1 parent 8d87c9b commit 8c329d2

File tree

11 files changed

+150
-25
lines changed

11 files changed

+150
-25
lines changed

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"devDependencies": {
4545
"@rslib/tsconfig": "workspace:*",
4646
"@types/fs-extra": "^11.0.4",
47+
"chokidar": "^4.0.1",
4748
"commander": "^12.1.0",
4849
"fs-extra": "^11.2.0",
4950
"memfs": "^4.14.0",

packages/core/prebundle.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export default {
1313
},
1414
dependencies: [
1515
'commander',
16+
'chokidar',
1617
{
1718
name: 'rslog',
1819
afterBundle(task) {

packages/core/rslib.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export default defineConfig({
2525
externals: {
2626
picocolors: '../compiled/picocolors/index.js',
2727
commander: '../compiled/commander/index.js',
28+
chokidar: '../compiled/chokidar/index.js',
2829
rslog: '../compiled/rslog/index.js',
2930
},
3031
},

packages/core/src/build.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,41 @@
11
import { type RsbuildInstance, createRsbuild } from '@rsbuild/core';
22
import type { BuildOptions } from './cli/commands';
3-
import { composeRsbuildEnvironments, pruneEnvironments } from './config';
4-
import type { RslibConfig } from './types/config';
3+
import {
4+
composeRsbuildEnvironments,
5+
loadConfig,
6+
pruneEnvironments,
7+
} from './config';
8+
import { onBeforeRestartServer, watchFilesForRestart } from './restart';
9+
10+
export async function build(options?: BuildOptions): Promise<RsbuildInstance> {
11+
const { content: config, filePath: configFilePath } = await loadConfig({
12+
path: options?.config,
13+
envMode: options?.envMode,
14+
});
515

6-
export async function build(
7-
config: RslibConfig,
8-
options?: BuildOptions,
9-
): Promise<RsbuildInstance> {
1016
const environments = await composeRsbuildEnvironments(config);
1117
const rsbuildInstance = await createRsbuild({
1218
rsbuildConfig: {
1319
environments: pruneEnvironments(environments, options?.lib),
1420
},
1521
});
1622

17-
await rsbuildInstance.build({
23+
const buildInstance = await rsbuildInstance.build({
1824
watch: options?.watch,
1925
});
2026

27+
if (options?.watch) {
28+
const files: string[] = [];
29+
files.push(configFilePath);
30+
31+
onBeforeRestartServer(buildInstance.close);
32+
33+
watchFilesForRestart(files, async () => {
34+
await build(options);
35+
});
36+
} else {
37+
await buildInstance.close();
38+
}
39+
2140
return rsbuildInstance;
2241
}

packages/core/src/cli/commands.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,7 @@ export function runCli(): void {
6060
.description('build the library for production')
6161
.action(async (options: BuildOptions) => {
6262
try {
63-
const rslibConfig = await loadConfig({
64-
path: options.config,
65-
envMode: options.envMode,
66-
});
67-
await build(rslibConfig, options);
63+
await build(options);
6864
} catch (err) {
6965
logger.error('Failed to build.');
7066
logger.error(err);
@@ -88,7 +84,7 @@ export function runCli(): void {
8884
.action(async (options: InspectOptions) => {
8985
try {
9086
// TODO: inspect should output Rslib's config
91-
const rslibConfig = await loadConfig({
87+
const { content: rslibConfig } = await loadConfig({
9288
path: options.config,
9389
envMode: options.envMode,
9490
});
@@ -115,7 +111,7 @@ export function runCli(): void {
115111
.description('start Rsbuild dev server of Module Federation format')
116112
.action(async (options: CommonOptions) => {
117113
try {
118-
const rslibConfig = await loadConfig({
114+
const { content: rslibConfig } = await loadConfig({
119115
path: options.config,
120116
envMode: options.envMode,
121117
});

packages/core/src/config.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,18 @@ export async function loadConfig({
112112
cwd?: string;
113113
path?: string;
114114
envMode?: string;
115-
}): Promise<RslibConfig> {
115+
}): Promise<{
116+
content: RslibConfig;
117+
filePath: string;
118+
}> {
116119
const configFilePath = resolveConfigPath(cwd, path);
117120
const { content } = await loadRsbuildConfig({
118121
cwd: dirname(configFilePath),
119122
path: configFilePath,
120123
envMode,
121124
});
122125

123-
return content as RslibConfig;
126+
return { content: content as RslibConfig, filePath: configFilePath };
124127
}
125128

126129
const composeExternalsWarnConfig = (

packages/core/src/restart.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import path from 'node:path';
2+
import chokidar from 'chokidar';
3+
import { color, debounce, isTTY } from './utils/helper';
4+
import { logger } from './utils/logger';
5+
6+
export async function watchFilesForRestart(
7+
files: string[],
8+
restart: () => Promise<void>,
9+
): Promise<void> {
10+
if (!files.length) {
11+
return;
12+
}
13+
14+
const watcher = chokidar.watch(files, {
15+
ignoreInitial: true,
16+
// If watching fails due to read permissions, the errors will be suppressed silently.
17+
ignorePermissionErrors: true,
18+
ignored: ['**/node_modules/**', '**/.git/**', '**/.DS_Store/**'],
19+
});
20+
21+
const callback = debounce(
22+
async (filePath) => {
23+
watcher.close();
24+
25+
await beforeRestart({ filePath });
26+
await restart();
27+
},
28+
// set 300ms debounce to avoid restart frequently
29+
300,
30+
);
31+
32+
watcher.on('add', callback);
33+
watcher.on('change', callback);
34+
watcher.on('unlink', callback);
35+
}
36+
37+
type Cleaner = () => Promise<unknown> | unknown;
38+
39+
let cleaners: Cleaner[] = [];
40+
41+
/**
42+
* Add a cleaner to handle side effects
43+
*/
44+
export const onBeforeRestartServer = (cleaner: Cleaner): void => {
45+
cleaners.push(cleaner);
46+
};
47+
48+
const clearConsole = () => {
49+
if (isTTY() && !process.env.DEBUG) {
50+
process.stdout.write('\x1B[H\x1B[2J');
51+
}
52+
};
53+
54+
const beforeRestart = async ({
55+
filePath,
56+
clear = true,
57+
}: {
58+
filePath?: string;
59+
clear?: boolean;
60+
} = {}): Promise<void> => {
61+
if (clear) {
62+
clearConsole();
63+
}
64+
65+
if (filePath) {
66+
const filename = path.basename(filePath);
67+
logger.info(`Restart because ${color.yellow(filename)} is changed.\n`);
68+
} else {
69+
logger.info('Restarting...\n');
70+
}
71+
72+
for (const cleaner of cleaners) {
73+
await cleaner();
74+
}
75+
cleaners = [];
76+
};

packages/core/src/utils/helper.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,4 +201,31 @@ export function checkMFPlugin(config: LibConfig): boolean {
201201
return added;
202202
}
203203

204+
export function debounce<T extends (...args: any[]) => void>(
205+
func: T,
206+
wait: number,
207+
): (...args: Parameters<T>) => void {
208+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
209+
210+
return (...args: Parameters<T>) => {
211+
if (timeoutId !== null) {
212+
clearTimeout(timeoutId);
213+
}
214+
215+
timeoutId = setTimeout(() => {
216+
func(...args);
217+
}, wait);
218+
};
219+
}
220+
221+
/**
222+
* Check if running in a TTY context
223+
*/
224+
export const isTTY = (type: 'stdin' | 'stdout' = 'stdout'): boolean => {
225+
return (
226+
(type === 'stdin' ? process.stdin.isTTY : process.stdout.isTTY) &&
227+
!process.env.CI
228+
);
229+
};
230+
204231
export { color };

packages/core/tests/config.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe('Should load config file correctly', () => {
99
test('Load config.js in cjs project', async () => {
1010
const fixtureDir = join(__dirname, 'fixtures/config/cjs');
1111
const configFilePath = join(fixtureDir, 'rslib.config.js');
12-
const config = await loadConfig({ path: configFilePath });
12+
const { content: config } = await loadConfig({ path: configFilePath });
1313
expect(config).toEqual({
1414
lib: [],
1515
source: {
@@ -26,7 +26,7 @@ describe('Should load config file correctly', () => {
2626
test('Load config.mjs in cjs project', async () => {
2727
const fixtureDir = join(__dirname, 'fixtures/config/cjs');
2828
const configFilePath = join(fixtureDir, 'rslib.config.mjs');
29-
const config = await loadConfig({ path: configFilePath });
29+
const { content: config } = await loadConfig({ path: configFilePath });
3030
expect(config).toEqual({
3131
lib: [],
3232
source: {
@@ -43,7 +43,7 @@ describe('Should load config file correctly', () => {
4343
test('Load config.ts in cjs project', async () => {
4444
const fixtureDir = join(__dirname, 'fixtures/config/cjs');
4545
const configFilePath = join(fixtureDir, 'rslib.config.ts');
46-
const config = await loadConfig({ path: configFilePath });
46+
const { content: config } = await loadConfig({ path: configFilePath });
4747
expect(config).toEqual({
4848
lib: [],
4949
source: {
@@ -60,7 +60,7 @@ describe('Should load config file correctly', () => {
6060
test('Load config.cjs with defineConfig in cjs project', async () => {
6161
const fixtureDir = join(__dirname, 'fixtures/config/cjs');
6262
const configFilePath = join(fixtureDir, 'rslib.config.cjs');
63-
const config = await loadConfig({ path: configFilePath });
63+
const { content: config } = await loadConfig({ path: configFilePath });
6464
expect(config).toEqual({
6565
lib: [],
6666
source: {
@@ -77,7 +77,7 @@ describe('Should load config file correctly', () => {
7777
test('Load config.js in esm project', async () => {
7878
const fixtureDir = join(__dirname, 'fixtures/config/esm');
7979
const configFilePath = join(fixtureDir, 'rslib.config.js');
80-
const config = await loadConfig({ path: configFilePath });
80+
const { content: config } = await loadConfig({ path: configFilePath });
8181
expect(config).toEqual({
8282
lib: [],
8383
source: {
@@ -94,7 +94,7 @@ describe('Should load config file correctly', () => {
9494
test('Load config.cjs in esm project', async () => {
9595
const fixtureDir = join(__dirname, 'fixtures/config/esm');
9696
const configFilePath = join(fixtureDir, 'rslib.config.cjs');
97-
const config = await loadConfig({ path: configFilePath });
97+
const { content: config } = await loadConfig({ path: configFilePath });
9898
expect(config).toEqual({
9999
lib: [],
100100
source: {
@@ -111,7 +111,7 @@ describe('Should load config file correctly', () => {
111111
test('Load config.ts in esm project', async () => {
112112
const fixtureDir = join(__dirname, 'fixtures/config/esm');
113113
const configFilePath = join(fixtureDir, 'rslib.config.ts');
114-
const config = await loadConfig({ path: configFilePath });
114+
const { content: config } = await loadConfig({ path: configFilePath });
115115
expect(config).toEqual({
116116
lib: [],
117117
source: {
@@ -128,7 +128,7 @@ describe('Should load config file correctly', () => {
128128
test('Load config.mjs with defineConfig in esm project', async () => {
129129
const fixtureDir = join(__dirname, 'fixtures/config/esm');
130130
const configFilePath = join(fixtureDir, 'rslib.config.mjs');
131-
const config = await loadConfig({ path: configFilePath });
131+
const { content: config } = await loadConfig({ path: configFilePath });
132132
expect(config).toMatchObject({
133133
lib: [],
134134
source: {

packages/core/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"moduleResolution": "Bundler",
1313
"paths": {
1414
"commander": ["./compiled/commander"],
15+
"chokidar": ["./compiled/chokidar"],
1516
"picocolors": ["./compiled/picocolors"],
1617
"rslog": ["./compiled/rslog"]
1718
}

tests/scripts/shared.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ export async function rslibBuild({
199199
path?: string;
200200
modifyConfig?: (config: RslibConfig) => void;
201201
}) {
202-
const rslibConfig = await loadConfig({
202+
const { content: rslibConfig } = await loadConfig({
203203
cwd,
204204
path,
205205
});

0 commit comments

Comments
 (0)