Skip to content

Commit 4ab749e

Browse files
committed
feat: support auto restart when config file changed
1 parent 2c04129 commit 4ab749e

14 files changed

Lines changed: 329 additions & 48 deletions

File tree

.changeset/purple-eels-provide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ice/pkg': minor
3+
---
4+
5+
feat: support auto restart when config file changed

packages/pkg/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"figures": "^6.1.0",
6363
"fs-extra": "^10.0.0",
6464
"get-tsconfig": "^4.13.0",
65-
"globby": "^11.0.4",
65+
"globby": "^14.0.1",
6666
"gzip-size": "^7.0.0",
6767
"http-proxy-middleware": "^3.0.5",
6868
"magic-string": "^0.25.7",

packages/pkg/src/cli.ts

Lines changed: 172 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,184 @@
11
import { fileURLToPath } from 'node:url';
22
import { consola } from 'consola';
3+
import * as chokidar from 'chokidar';
34
import { cac } from 'cac';
45
import { readFileSync } from 'node:fs';
56
import { join, dirname } from 'node:path';
67
import { createCore } from './core/create.js';
8+
import type { FSWatcher } from 'chokidar';
9+
import type { StartCommandHandle, StartCloseReason, BuildCommandHandle } from './types.js';
10+
import { createBatchChangeHandler } from './helpers/watcher.js';
711

812
const __dirname = dirname(fileURLToPath(import.meta.url));
913

1014
const cli = cac('ice-pkg');
1115

16+
function getSignalExitCode(signal: NodeJS.Signals): number {
17+
return signal === 'SIGINT' ? 130 : 143;
18+
}
19+
20+
function exitWithSignal(signal: NodeJS.Signals): never {
21+
process.exit(getSignalExitCode(signal));
22+
}
23+
24+
async function runBuildWithSignalClose(options: { rootDir: string; [key: string]: unknown }) {
25+
delete options['--'];
26+
const { rootDir, ...commandArgs } = options;
27+
28+
const pkg = await createCore({
29+
rootDir,
30+
command: 'build',
31+
commandArgs,
32+
});
33+
34+
let commandHandle: BuildCommandHandle | null = null;
35+
36+
const shutdown = (signal: NodeJS.Signals) => {
37+
exitWithSignal(signal);
38+
};
39+
40+
const onSigint = () => shutdown('SIGINT');
41+
const onSigterm = () => shutdown('SIGTERM');
42+
process.on('SIGINT', onSigint);
43+
process.on('SIGTERM', onSigterm);
44+
45+
try {
46+
commandHandle = (await pkg.run()) as BuildCommandHandle;
47+
48+
if (commandHandle.error) {
49+
await commandHandle.dispose('build-error');
50+
throw commandHandle.error;
51+
}
52+
53+
await commandHandle.dispose('build-finished');
54+
} finally {
55+
process.off('SIGINT', onSigint);
56+
process.off('SIGTERM', onSigterm);
57+
}
58+
}
59+
60+
async function runStartWithConfigRestart(options: { rootDir: string; [key: string]: unknown }) {
61+
delete options['--'];
62+
const { rootDir, ...commandArgs } = options;
63+
64+
let currentPkg: Awaited<ReturnType<typeof createCore>> | null = null;
65+
let currentCommand: StartCommandHandle | null = null;
66+
let currentCommandClosed = false;
67+
let configWatcher: FSWatcher | null = null;
68+
let watchedConfigFile: string | null = null;
69+
let shuttingDown = false;
70+
let disposing = false;
71+
72+
const batchHandler = createBatchChangeHandler(restartStartRuntime);
73+
74+
async function createAndRunStart() {
75+
const pkg = await createCore({
76+
rootDir,
77+
command: 'start',
78+
commandArgs,
79+
});
80+
const runtime = (await pkg.run()) as StartCommandHandle;
81+
currentPkg = pkg;
82+
currentCommand = runtime;
83+
currentCommandClosed = false;
84+
}
85+
86+
async function closeCurrentRuntime(reason: StartCloseReason) {
87+
if (!currentCommand || currentCommandClosed) {
88+
return;
89+
}
90+
currentCommandClosed = true;
91+
disposing = true;
92+
try {
93+
await currentCommand.dispose(reason);
94+
} finally {
95+
disposing = false;
96+
}
97+
}
98+
99+
async function updateConfigWatcher() {
100+
const nextConfigFile = currentPkg?.resolvedConfigFile ?? null;
101+
if (nextConfigFile === watchedConfigFile) {
102+
return;
103+
}
104+
105+
await configWatcher?.close();
106+
configWatcher = null;
107+
watchedConfigFile = nextConfigFile;
108+
109+
if (!watchedConfigFile) {
110+
return;
111+
}
112+
113+
configWatcher = chokidar.watch([watchedConfigFile], {
114+
ignoreInitial: true,
115+
ignorePermissionErrors: true,
116+
});
117+
118+
configWatcher.on('add', batchHandler.onChange);
119+
configWatcher.on('change', batchHandler.onChange);
120+
configWatcher.on('unlink', batchHandler.onChange);
121+
configWatcher.on('error', (error) => consola.error(error));
122+
}
123+
124+
async function restartStartRuntime() {
125+
if (shuttingDown) {
126+
return;
127+
}
128+
129+
try {
130+
consola.info('Configuration changed, restarting...');
131+
await closeCurrentRuntime('config-change');
132+
await createAndRunStart();
133+
await updateConfigWatcher();
134+
} catch (error) {
135+
consola.error(error);
136+
}
137+
}
138+
139+
async function shutdown(signal: NodeJS.Signals) {
140+
if ((signal === 'SIGINT' && disposing) || shuttingDown) {
141+
exitWithSignal(signal);
142+
}
143+
shuttingDown = true;
144+
145+
await configWatcher?.close();
146+
configWatcher = null;
147+
148+
const shutdownHintTimer =
149+
signal === 'SIGINT'
150+
? setTimeout(() => {
151+
consola.warn('Still shutting down. Please wait, or press Ctrl+C again to force exit.');
152+
}, 1000)
153+
: null;
154+
155+
try {
156+
await closeCurrentRuntime('signal-exit');
157+
} catch (error) {
158+
consola.error(error);
159+
} finally {
160+
if (shutdownHintTimer) {
161+
clearTimeout(shutdownHintTimer);
162+
}
163+
}
164+
165+
exitWithSignal(signal);
166+
}
167+
168+
const onSigint = () => {
169+
void shutdown('SIGINT');
170+
};
171+
const onSigterm = () => {
172+
void shutdown('SIGTERM');
173+
};
174+
175+
process.on('SIGINT', onSigint);
176+
process.on('SIGTERM', onSigterm);
177+
178+
await createAndRunStart();
179+
await updateConfigWatcher();
180+
}
181+
12182
(async () => {
13183
cli
14184
.command('build', 'Bundle files', {
@@ -22,16 +192,7 @@ const cli = cac('ice-pkg');
22192
default: process.cwd(),
23193
})
24194
.action(async (options) => {
25-
delete options['--'];
26-
const { rootDir, ...commandArgs } = options;
27-
28-
const pkg = await createCore({
29-
rootDir: options.rootDir,
30-
command: 'build',
31-
commandArgs,
32-
});
33-
34-
await pkg.run();
195+
await runBuildWithSignalClose(options);
35196
});
36197

37198
cli
@@ -49,16 +210,7 @@ const cli = cac('ice-pkg');
49210
.option('--port <port>', 'Override default server port', {})
50211
.option('--host <host>', 'Override default server host', {})
51212
.action(async (options) => {
52-
delete options['--'];
53-
const { rootDir, ...commandArgs } = options;
54-
55-
const pkg = await createCore({
56-
rootDir: options.rootDir,
57-
command: 'start',
58-
commandArgs,
59-
});
60-
61-
await pkg.run();
213+
await runStartWithConfigRestart(options);
62214
});
63215

64216
cli.help();

packages/pkg/src/commands/build.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
import fse from 'fs-extra';
2-
import type { BuildTask, Context, OutputResult } from '../types.js';
2+
import type { BuildTask, Context, OutputResult, BuildCloseReason } from '../types.js';
33
import { createRunnerReporter } from '../helpers/runnerReporter.js';
44
import { getTaskRunners } from '../helpers/getTaskRunners.js';
55
import { RunnerScheduler } from '../helpers/runnerScheduler.js';
66

77
export default async function build(context: Context) {
88
const { applyHook, commandArgs } = context;
9+
let closePromise: Promise<void> | null = null;
10+
let error: unknown;
11+
let outputResults: OutputResult[] | undefined;
12+
13+
async function dispose(reason: BuildCloseReason) {
14+
if (closePromise) {
15+
return closePromise;
16+
}
17+
closePromise = (async () => {
18+
await applyHook('before.build.close', reason);
19+
await taskGroup?.close();
20+
})();
21+
return closePromise;
22+
}
923

1024
const buildTasks = context.getTaskConfig() as BuildTask[];
1125
const taskConfigs = buildTasks.map(({ config }) => config);
@@ -24,26 +38,29 @@ export default async function build(context: Context) {
2438
config: taskConfigs,
2539
});
2640

27-
// Empty outputDir before run the task.
2841
const outputDirs = taskConfigs.map((config) => config.outputDir!).filter(Boolean);
2942
outputDirs.forEach((outputDir) => fse.emptyDirSync(outputDir));
3043

3144
const tasks = getTaskRunners(buildTasks, context);
45+
const terminal = createRunnerReporter();
46+
const taskGroup = new RunnerScheduler(tasks, terminal);
3247

3348
try {
34-
const terminal = createRunnerReporter();
35-
const taskGroup = new RunnerScheduler(tasks, terminal);
36-
3749
const results = taskGroup.run();
38-
const outputResults: OutputResult[] = await results;
50+
outputResults = await results;
3951

4052
await applyHook('after.build.compile', outputResults);
4153
} catch (err) {
54+
error = err;
4255
await applyHook('error', {
4356
errCode: 'COMPILE_ERROR',
4457
err,
4558
});
46-
47-
throw err;
4859
}
60+
61+
return {
62+
dispose,
63+
error,
64+
outputResults,
65+
};
4966
}

packages/pkg/src/commands/start.ts

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { consola } from 'consola';
22
import { createBatchChangeHandler, createWatcher } from '../helpers/watcher.js';
3-
import type { OutputResult, Context, WatchChangedFile, BuildTask } from '../types.js';
3+
import type {
4+
OutputResult,
5+
Context,
6+
WatchChangedFile,
7+
BuildTask,
8+
StartCommandHandle,
9+
StartCloseReason,
10+
} from '../types.js';
411
import { createRunnerReporter } from '../helpers/runnerReporter.js';
512
import { getTaskRunners } from '../helpers/getTaskRunners.js';
613
import { RunnerScheduler } from '../helpers/runnerScheduler.js';
@@ -36,19 +43,49 @@ export default async function start(context: Context) {
3643
})
3744
: null;
3845
const batchHandler = createBatchChangeHandler(runChangedCompile);
46+
const tasks = getTaskRunners(buildTasks, context, watcher);
47+
const terminal = createRunnerReporter();
48+
const taskGroup = new RunnerScheduler(tasks, terminal);
49+
let runningCompile: Promise<unknown> | null = null;
50+
let disposed = false;
51+
let disposePromise: Promise<void> | null = null;
52+
53+
function trackCompile<T>(compileTask: Promise<T>): Promise<T> {
54+
const tracked = compileTask.finally(() => {
55+
runningCompile = null;
56+
});
57+
runningCompile = tracked.catch(() => {});
58+
return tracked;
59+
}
60+
61+
async function dispose(reason: StartCloseReason) {
62+
if (disposePromise) {
63+
return disposePromise;
64+
}
65+
66+
disposePromise = (async () => {
67+
if (disposed) {
68+
return;
69+
}
70+
disposed = true;
71+
await runningCompile;
72+
await applyHook('before.start.close', reason);
73+
await taskGroup?.close();
74+
await watcher.close();
75+
await devServer?.close();
76+
})();
77+
78+
return disposePromise;
79+
}
80+
3981
batchHandler.beginBlock();
4082

4183
watcher.on('add', (id) => batchHandler.onChange(id, 'create'));
4284
watcher.on('change', (id) => batchHandler.onChange(id, 'update'));
4385
watcher.on('unlink', (id) => batchHandler.onChange(id, 'delete'));
4486
watcher.on('error', (error) => consola.error(error));
4587

46-
const tasks = getTaskRunners(buildTasks, context, watcher);
47-
48-
const terminal = createRunnerReporter();
49-
const taskGroup = new RunnerScheduler(tasks, terminal);
50-
51-
const outputResults: OutputResult[] = await taskGroup.run();
88+
const outputResults: OutputResult[] = await trackCompile(taskGroup.run());
5289

5390
await applyHook('after.start.compile', outputResults);
5491

@@ -57,14 +94,23 @@ export default async function start(context: Context) {
5794
batchHandler.endBlock();
5895

5996
async function runChangedCompile(changedFiles: WatchChangedFile[]) {
97+
if (disposed) {
98+
return;
99+
}
100+
60101
try {
61-
const newOutputResults: OutputResult[] = await taskGroup.run(changedFiles);
102+
const newOutputResults: OutputResult[] = await trackCompile(taskGroup.run(changedFiles));
62103

63104
await applyHook('after.start.compile', newOutputResults);
64105
} catch (error) {
65106
consola.error(error);
66107
}
67108
}
68109

69-
return watcher;
110+
const runtimeHandle: StartCommandHandle = {
111+
watcher,
112+
dispose,
113+
};
114+
115+
return runtimeHandle;
70116
}

0 commit comments

Comments
 (0)